Skip to content
2,848 linesCodeBlameRaw

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

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 'main' into worktree-agent-a69aeabc4b0deeb978mod about;
Merge branch 'worktree-agent-ac5b181a013e54348'9mod backups;
Agents as a team: lifecycle, merge queue, billing and a new shell10mod blame;
Catching up with main takes seconds when the two sides touched different files11mod catch_up;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily12mod coalesce;
Fast pages, required checks on the branch, self-hosted runners, honest incidents13mod commit_file;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9714mod contributors;
Diffs on attempts; hosted agent presented as the g1t agent15mod diff;
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 2516mod drift;
Merge branch 'worktree-agent-a2013627e5ea4ab13'17mod fallback;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily18mod forks;
Rust repos service with shipping; pull requests kept in the model19mod git_http;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look20mod git_ops;
Agents as a team: lifecycle, merge queue, billing and a new shell21mod import;
Rust repos service with shipping; pull requests kept in the model22mod land;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9723mod languages;
Branches and Tags pages, each file's last commit, and the branch menu on files24mod last_commits;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9725mod license;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look26mod lifecycle;
Search across all of g1t, Explore, and a command palette27mod listing;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily28mod meters;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look29mod mirror;
Merge branch 'worktree-agent-a2013627e5ea4ab13'30mod moves;
31mod namespaces;
Merge branch 'worktree-agent-a1b995daa94e4e1b7'32mod pack_cache;
Fast pages, required checks on the branch, self-hosted runners, honest incidents33mod pack_limits;
Pull requests from branches34mod refs;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms35mod refs_cache;
Rust repos service with shipping; pull requests kept in the model36mod registry;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily37mod resilience;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge38mod rule_facts;
39mod rules;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API40mod run_access;
41mod secret_scan;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily42mod shards;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms43mod shared;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge44mod signatures;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9745mod stats;
Rust repos service with shipping; pull requests kept in the model46mod store;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look47mod transfer;
Workflow files need workflow_files:write from a token; fine-grained permission table48mod workflow_gate;
Rust repos service with shipping; pull requests kept in the model49
Agents and memory, checks and conflicts, profiles, slug renames, custom domains50use g1t_contracts::events::{
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look51 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 member52 WorkspaceDeleting, WorkspaceRenamed, WorkspaceRestored,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains53};
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look54use g1t_contracts::access::{self, Capability};
Rust repos service with shipping; pull requests kept in the model55use g1t_contracts::repos::*;
RFC 3339 timestamps in identity and repos56use g1t_contracts::time::rfc3339;
Merge main (membership, two-factor, GitHub repo roles) into tokens57use g1t_contracts::{FailureCode, Outcome, PrincipalKind, Role, User, Viewer, is_valid_repo_name, new_id};
Events service in Rust, with RFC 3339 times and accurate push events58use g1t_kit::{args, now_ms, reply, rpc_method};
Diffs on attempts; hosted agent presented as the g1t agent59use std::collections::{HashMap, HashSet, VecDeque};
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms60use std::rc::Rc;
Rust repos service with shipping; pull requests kept in the model61
62use serde::Serialize;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look63use worker::{
64 Context, Env, Fetcher, MessageBatch, Method, Request, Response, Result, ScheduleContext, ScheduledEvent,
65 event,
66};
Rust repos service with shipping; pull requests kept in the model67
68use registry::{Registry, can_read, can_write, store_key};
69use store::{ArtifactsStore, GitRepo, GitStore, Scope};
70
Issues and pull requests replace intents and attempts71/// 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 daily72pub(crate) const PULLS_NAMESPACE: &str = "pulls";
Rust repos service with shipping; pull requests kept in the model73const MAX_TEXT_BYTES: usize = 512 * 1024;
Issues and pull requests replace intents and attempts74/// How far back a pull request may have forked and still be landed.
Rust repos service with shipping; pull requests kept in the model75const MAX_ANCESTRY: u32 = 1000;
Branches and Tags pages, each file's last commit, and the branch menu on files76/// The most tags a repository's Tags page reads and lists.
77const MAX_TAGS_READ: usize = 100;
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 2578/// Branch heads measured in one `branch_drift` call.
79const MAX_DRIFT_HEADS: usize = 100;
Branches and Tags pages, each file's last commit, and the branch menu on files80
81/// One path segment, percent-encoded for a cache key.
82fn urlencoding_segment(segment: &str) -> String {
83 segment
84 .bytes()
85 .map(|b| if b.is_ascii_alphanumeric() || b"-._~".contains(&b) { (b as char).to_string() } else { format!("%{b:02X}") })
86 .collect()
87}
Agents as a team: lifecycle, merge queue, billing and a new shell88const MAX_DESCRIPTION_CHARS: usize = 200;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look89pub(crate) const SOURCE: &str = "repos";
90pub(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 model91
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look92pub(crate) fn not_found<T>() -> Outcome<T> {
Rust repos service with shipping; pull requests kept in the model93 Outcome::fail(FailureCode::NotFound, "Repository not found.")
94}
95
96/// Decoded text, or `None` when the file is too large or looks binary.
Agents as a team: lifecycle, merge queue, billing and a new shell97/// Whether a ref is a full commit hash rather than a branch name.
98fn is_commit_hash(git_ref: &str) -> bool {
99 git_ref.len() == 40 && git_ref.bytes().all(|b| b.is_ascii_hexdigit())
100}
101
Rust repos service with shipping; pull requests kept in the model102fn text_of(bytes: Vec<u8>) -> Option<String> {
103 if bytes.len() > MAX_TEXT_BYTES || bytes.contains(&0) {
104 return None;
105 }
106 Some(String::from_utf8_lossy(&bytes).into_owned())
107}
108
109fn is_readme(name: &str) -> bool {
110 matches!(
111 name.to_lowercase().as_str(),
112 "readme" | "readme.md" | "readme.markdown" | "readme.txt"
113 )
114}
115
116/// Whether `ancestor` is reachable from the newest commit in `history`.
117///
118/// `history` is the first-parent chain, which is all the store lists; a fork
119/// that merged the target branch in has the target's head on a second
120/// parent, so the walk follows every parent.
121async fn descends_from<R: GitRepo>(repo: &R, history: &[Commit], ancestor: &str) -> Result<bool> {
122 let known: HashMap<&str, &[String]> = history
123 .iter()
124 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
125 .collect();
126 let mut seen = HashSet::new();
127 let mut queue: Vec<String> = history
128 .first()
129 .map(|c| c.hash.clone())
130 .into_iter()
131 .collect();
132 while let Some(hash) = queue.pop() {
133 if hash == ancestor {
134 return Ok(true);
135 }
136 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
137 continue;
138 }
139 match known.get(hash.as_str()) {
140 Some(parents) => queue.extend(parents.iter().cloned()),
141 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
142 }
143 }
144 Ok(false)
145}
146
Diffs on attempts; hosted agent presented as the g1t agent147/// The commit closest to the newest in `history` that is also in `shared`:
148/// where a fork and the repository it came from last agreed.
149async fn nearest_ancestor_in<R: GitRepo>(
150 repo: &R,
151 history: &[Commit],
152 shared: &HashSet<String>,
153) -> Result<Option<String>> {
154 let known: HashMap<&str, &[String]> = history
155 .iter()
156 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
157 .collect();
158 let mut seen = HashSet::new();
159 let mut queue: VecDeque<String> = history
160 .first()
161 .map(|c| c.hash.clone())
162 .into_iter()
163 .collect();
164 while let Some(hash) = queue.pop_front() {
165 if shared.contains(&hash) {
166 return Ok(Some(hash));
167 }
168 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
169 continue;
170 }
171 match known.get(hash.as_str()) {
172 Some(parents) => queue.extend(parents.iter().cloned()),
173 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
174 }
175 }
176 Ok(None)
177}
178
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily179thread_local! {
180 /// Targets' sides of mergeability, by head (coalesce.rs).
181 static TARGETS: std::cell::RefCell<coalesce::Memo<coalesce::TargetKey, Rc<coalesce::TargetSide>>> =
182 std::cell::RefCell::new(coalesce::Memo::new(coalesce::TARGET_TTL_MS, 32));
183 /// What targets changed between two trees.
184 static THEIRS: std::cell::RefCell<coalesce::Memo<coalesce::TheirsKey, (Vec<String>, bool)>> =
185 std::cell::RefCell::new(coalesce::Memo::new(coalesce::THEIRS_TTL_MS, 256));
186 /// What repositories hold, as read for a push's first request, for the
187 /// same push's second: a push's POST does not wait on the database.
188 static HELD: std::cell::RefCell<coalesce::Memo<String, u64>> =
189 std::cell::RefCell::new(coalesce::Memo::new(60_000, 512));
190}
191
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look192pub(crate) struct Repos<S: GitStore> {
Rust repos service with shipping; pull requests kept in the model193 registry: Registry,
194 store: S,
Events service in Rust, with RFC 3339 times and accurate push events195 events: Fetcher,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API196 /// Asked during a push which secrets have been allowed.
197 security: Option<Fetcher>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look198 /// Asked whether a workspace is on a plan, for its private storage.
199 billing: Option<Fetcher>,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge200 /// Says which rulesets hold for a change to a branch or tag, and keeps
201 /// how they judged it (rules.rs). `None` where it is not deployed: the
202 /// old protection flag then holds on push.
203 work: Option<Fetcher>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look204 /// Told when a repository moves, for the tokens of agents at work on it.
205 identity: Option<Fetcher>,
206 /// What a free workspace's private repositories may hold.
207 free_private_bytes: i64,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily208 /// Days a pull request's working copy is kept after it settles (forks.rs).
209 pub(crate) fork_days: u64,
210 /// The most a repository may hold (pack_limits.rs), and what happens
211 /// to a push too large to scan.
212 repo_limit: u64,
213 large_pushes: git_http::LargePushes,
Merge branch 'worktree-agent-a2013627e5ea4ab13'214 /// Which git store namespace new repositories go in (shards.rs), and
215 /// the most each should hold (`ARTIFACTS_NAMESPACE_LIMITS`).
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily216 placement: shards::Placement,
Merge branch 'worktree-agent-a2013627e5ea4ab13'217 limits: HashMap<String, u64>,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms218 /// What isolates share: answers that list refs (refs_cache.rs).
219 shared: Option<Rc<shared::Shared>>,
Merge branch 'worktree-agent-a1b995daa94e4e1b7'220 /// Packs for fresh clones (pack_cache.rs); `None` without the bucket.
Merge branch 'worktree-agent-aaf03bdceac799c89'221 packs: Option<Rc<pack_cache::Packs>>,
Rust repos service with shipping; pull requests kept in the model222}
223
224impl<S: GitStore> Repos<S> {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms225 /// Records that the refs of the repository with this id changed, once
226 /// they have, so that the answers kept that list them go stale (see
227 /// refs_cache.rs). Everything that changes a repository's refs calls
228 /// this after it (`every_ref_writer_records_the_change` checks). A
229 /// failure is logged: the change itself happened, and what was kept
230 /// expires within `refs_cache::TTL_SECONDS` regardless.
231 pub(crate) async fn refs_moved(&self, repo_id: &str) {
232 if let Err(error) = self.registry.refs_moved(repo_id).await {
233 worker::console_error!("refs of {repo_id} changed but not recorded: {error}");
234 }
235 }
236
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look237 pub(crate) async fn publish<T: Serialize>(&self, event: NewEvent<T>) -> Result<()> {
Events service in Rust, with RFC 3339 times and accurate push events238 g1t_kit::call(
239 &self.events,
240 "publish",
241 &Publish {
242 events: vec![event],
243 },
244 )
245 .await
Rust repos service with shipping; pull requests kept in the model246 }
247
Members can read a private repository's pull request forks248 /// Whether the viewer may read `repo`. A pull request's fork of a
249 /// private repository can be read by everyone who can read that
250 /// repository, so its members can review and check out the change, as
251 /// well as by whoever opened the pull request.
252 async fn may_read(&self, repo: &Repo, viewer: &Viewer) -> Result<bool> {
253 if can_read(repo, viewer) {
254 return Ok(true);
255 }
256 let Some(source_id) = &repo.fork_of else {
257 return Ok(false);
258 };
Rust repos service with shipping; pull requests kept in the model259 Ok(self
260 .registry
Members can read a private repository's pull request forks261 .by_id(source_id)
Rust repos service with shipping; pull requests kept in the model262 .await?
Members can read a private repository's pull request forks263 .is_some_and(|source| can_read(&source, viewer)))
Rust repos service with shipping; pull requests kept in the model264 }
265
Members can read a private repository's pull request forks266 /// `repo`, if there is one and the viewer may read it.
267 async fn visible(&self, repo: Option<Repo>, viewer: &Viewer) -> Result<Option<Repo>> {
268 Ok(match repo {
269 Some(repo) if self.may_read(&repo, viewer).await? => Some(repo),
270 _ => None,
271 })
272 }
273
274 /// 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 look275 pub(crate) async fn readable(&self, path: &RepoPath, viewer: &Viewer) -> Result<Option<Repo>> {
Members can read a private repository's pull request forks276 self.visible(self.registry.by_path(path).await?, viewer)
277 .await
278 }
279
Rust repos service with shipping; pull requests kept in the model280 async fn get(&self, a: GetArgs) -> Result<Outcome<Repo>> {
281 Ok(self
282 .readable(&a.path, &a.viewer)
283 .await?
284 .map_or_else(not_found, Outcome::Ok))
285 }
286
287 async fn get_by_id(&self, a: GetByIdArgs) -> Result<Outcome<Repo>> {
288 Ok(self
Members can read a private repository's pull request forks289 .visible(self.registry.by_id(&a.id).await?, &a.viewer)
Rust repos service with shipping; pull requests kept in the model290 .await?
291 .map_or_else(not_found, Outcome::Ok))
292 }
293
Agents as a team: lifecycle, merge queue, billing and a new shell294 async fn update(&self, a: UpdateArgs) -> Result<Outcome<Repo>> {
295 let viewer = Some(a.actor.clone());
296 let Some(repo) = self.readable(&a.path, &viewer).await? else {
297 return Ok(not_found());
298 };
Merge main (membership, two-factor, GitHub repo roles) into tokens299 // Its details take Maintain; its protection, Admin; who can see it,
300 // Admin and the member privileges (below). See g1t_contracts::access.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look301 let protection_changes = a.protected.is_some_and(|protected| protected != repo.protected);
302 let details_change = a.description.is_some() || a.website.is_some() || a.topics.is_some();
303 let mut needed = Vec::new();
304 if details_change || !protection_changes {
305 needed.push(Capability::ManageSettings);
306 }
307 if protection_changes {
308 needed.push(Capability::ManageProtection);
309 }
310 let full_name = format!("{}/{}", repo.namespace, repo.name);
311 if repo.fork_of.is_some() {
312 return Ok(Outcome::fail(FailureCode::Forbidden, access::needs(Capability::ManageSettings, &full_name)));
313 }
314 if let Some(missing) = needed.into_iter().find(|capability| !registry::can(&repo, &viewer, *capability)) {
315 return Ok(Outcome::fail(FailureCode::Forbidden, access::needs(missing, &full_name)));
Agents as a team: lifecycle, merge queue, billing and a new shell316 }
317 if !a.actor.verified {
318 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
319 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look320 if let Some((code, message)) = lifecycle::archived_refusal(&repo) {
321 return Ok(Outcome::fail(code, message));
322 }
Agents as a team: lifecycle, merge queue, billing and a new shell323 let description = match a.description {
324 Some(text) => Some(
325 text.trim()
326 .chars()
327 .take(MAX_DESCRIPTION_CHARS)
328 .collect::<String>(),
329 )
330 .filter(|text| !text.is_empty()),
331 None => repo.description.clone(),
332 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look333 let website = match a.website.as_deref() {
334 Some(text) => match clean_website(text) {
335 Ok(website) => website,
336 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
337 },
338 None => repo.website.clone(),
339 };
340 // Who can see it is an owner's to change, and a free workspace's
341 // storage may not take it private: see lifecycle.rs.
342 let wants_private = a.is_private.filter(|private| *private != repo.is_private);
343 if wants_private.is_some()
344 && let Err((code, message)) = lifecycle::admin_only(
345 lifecycle::Asker::on(&a.actor, &repo),
346 &repo.namespace,
347 "change the visibility of",
Merge main (membership, two-factor, GitHub repo roles) into tokens348 Capability::ChangeVisibility,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look349 )
350 {
351 return Ok(Outcome::fail(code, message));
352 }
Merge main (membership, two-factor, GitHub repo roles) into tokens353 if let Some(private) = wants_private
354 && let Some(why) = lifecycle::visibility_refusal(&a.actor, &repo, private)
355 {
356 return Ok(Outcome::fail(FailureCode::Forbidden, why));
357 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look358 let is_private = repo.is_private;
Agents as a team: lifecycle, merge queue, billing and a new shell359 let protected = a.protected.unwrap_or(repo.protected);
Search across all of g1t, Explore, and a command palette360 let topics = match &a.topics {
361 Some(topics) => match clean_topics(topics) {
362 Ok(topics) => topics,
363 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
364 },
365 None => repo.topics.clone(),
366 };
Agents as a team: lifecycle, merge queue, billing and a new shell367 self.registry
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look368 .update(&repo.id, description.as_deref(), protected, &topics, website.as_deref())
Agents as a team: lifecycle, merge queue, billing and a new shell369 .await?;
Search across all of g1t, Explore, and a command palette370 let updated = Repo {
Agents as a team: lifecycle, merge queue, billing and a new shell371 description,
372 is_private,
373 protected,
Search across all of g1t, Explore, and a command palette374 topics,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look375 website,
Agents as a team: lifecycle, merge queue, billing and a new shell376 ..repo
Search across all of g1t, Explore, and a command palette377 };
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge378 // Whether the default branch takes only pull requests is now its
379 // branch protection ruleset's to say (work's rulesets.rs).
380 if let (Some(protected), Some(work)) = (a.protected, &self.work) {
381 #[derive(Serialize)]
382 struct RequirePullRequest<'a> {
383 repo: &'a Repo,
384 protected: bool,
385 actor: &'a User,
386 }
387 let set: Result<Outcome<bool>> =
388 g1t_kit::call(work, "set_requires_pull_request", &RequirePullRequest { repo: &updated, protected, actor: &a.actor }).await;
389 match set {
390 Ok(Outcome::Ok(_)) => {}
391 Ok(Outcome::Fail(failure)) => return Ok(Outcome::Fail(failure)),
392 Err(error) => return Err(error),
393 }
394 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look395 if let Some(private) = wants_private {
396 return self.change_visibility(updated, private, &a.actor, a.surface).await;
397 }
398 let visibility_changed = false;
Search across all of g1t, Explore, and a command palette399 // Search and anything else that shows the repository hears of it;
400 // a change of visibility is announced on its own as well, so that
401 // what was public stops being shown at once.
402 self.publish(NewEvent {
403 kind: "repo.updated",
404 source: SOURCE,
405 repo_id: Some(updated.id.clone()),
406 actor: Some(a.actor.id.clone()),
407 data: RepoUpdated {
408 repo_id: updated.id.clone(),
409 namespace: updated.namespace.clone(),
410 name: updated.name.clone(),
411 is_private,
412 visibility_changed,
413 },
414 })
415 .await?;
416 Ok(Outcome::Ok(updated))
417 }
418
419 /// The repository with this id, if it is not a fork, and its store.
420 async fn stored(&self, repo_id: &str) -> Result<Option<S::Repo>> {
421 match self.registry.by_id(repo_id).await? {
422 Some(repo) if repo.fork_of.is_none() => Ok(Some(self.store.open(&store_key(&repo)).await?)),
423 _ => Ok(None),
424 }
425 }
426
427 async fn list_files(&self, a: ListFilesArgs) -> Result<FileList> {
428 let Some(repo) = self.registry.by_id(&a.repo_id).await?.filter(|repo| repo.fork_of.is_none()) else {
429 return Ok(FileList::default());
430 };
431 let git = self.store.open(&store_key(&repo)).await?;
432 let head = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
433 listing::list(&git, None, &head, &a.skip_dirs, a.limit).await
434 }
435
436 async fn changed_files(&self, a: ChangedFilesArgs) -> Result<FileList> {
437 let Some(git) = self.stored(&a.repo_id).await? else {
438 return Ok(FileList::default());
439 };
440 listing::list(&git, a.base.as_deref(), &a.head, &a.skip_dirs, a.limit).await
441 }
442
Composer from the workspace's own repositories, and go get from g1t.sh443 /// Branches and tags with their commits, for g1t's own services.
444 async fn refs_of(&self, a: RefsArgs) -> Result<Option<RepoRefs>> {
445 let Some(repo) = self.registry.by_id(&a.repo_id).await?.filter(|repo| repo.fork_of.is_none()) else {
446 return Ok(None);
447 };
448 let git = self.store.open(&store_key(&repo)).await?;
449 let access = git.access(Scope::Read).await?;
450 let refs = refs::heads_and_tags(refs::all(&access).await?)
451 .into_iter()
452 .map(|(name, commit)| GitRefEntry { name, commit })
453 .collect();
454 Ok(Some(RepoRefs { repo, refs }))
455 }
456
457 async fn raw_file(&self, a: RawFileArgs) -> Result<Option<RawFile>> {
458 use base64::Engine;
459 let Some(git) = self.stored(&a.repo_id).await? else {
460 return Ok(None);
461 };
462 Ok(git
463 .read_file(&a.git_ref, &a.path)
464 .await?
465 .filter(|bytes| bytes.len() <= a.max_bytes as usize)
466 .map(|bytes| RawFile { size: bytes.len() as u64, data: base64::engine::general_purpose::STANDARD.encode(bytes) }))
467 }
468
469 async fn raw_blobs(&self, a: RawBlobsArgs) -> Result<Vec<RawBlob>> {
470 use base64::Engine;
471 let Some(git) = self.stored(&a.repo_id).await? else {
472 return Ok(Vec::new());
473 };
474 let hashes: Vec<&String> = a.hashes.iter().take(MAX_READ_BLOBS).collect();
475 let mut out = Vec::with_capacity(hashes.len());
476 // A few at a time, as listing::read does: each is a round trip.
477 for group in hashes.chunks(8) {
478 let read = futures_util::future::try_join_all(group.iter().map(|hash| git.read_blob(hash))).await?;
479 for (hash, bytes) in group.iter().zip(read) {
480 let size = bytes.as_ref().map_or(0, |bytes| bytes.len() as u64);
481 let data = bytes
482 .filter(|bytes| bytes.len() <= a.max_bytes as usize)
483 .map(|bytes| base64::engine::general_purpose::STANDARD.encode(bytes));
484 out.push(RawBlob { hash: (*hash).clone(), size, data });
485 }
486 }
487 Ok(out)
488 }
489
Search across all of g1t, Explore, and a command palette490 async fn read_blobs(&self, a: ReadBlobsArgs) -> Result<Vec<BlobText>> {
491 let Some(git) = self.stored(&a.repo_id).await? else {
492 return Ok(Vec::new());
493 };
494 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 shell495 }
496
Rust repos service with shipping; pull requests kept in the model497 async fn create(&self, a: CreateArgs) -> Result<Outcome<Repo>> {
498 if !a.owner.verified {
499 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
500 }
501 let name = a.name.trim().to_lowercase();
502 if !is_valid_repo_name(&name) {
503 return Ok(Outcome::fail(
504 FailureCode::Invalid,
505 "Use letters, digits, dots, hyphens and underscores only.",
506 ));
507 }
Workspaces own repositories508 let namespace = a.namespace.trim().to_lowercase();
509 if namespace.is_empty() {
Rust repos service with shipping; pull requests kept in the model510 return Ok(Outcome::fail(
511 FailureCode::Invalid,
Workspaces own repositories512 "Say which workspace to create the repository in.",
513 ));
514 }
Merge main (membership, two-factor, GitHub repo roles) into tokens515 let Some(role) = a.owner.role_in(&namespace) else {
Workspaces own repositories516 return Ok(Outcome::fail(
517 FailureCode::Forbidden,
518 "You are not a member of that workspace.",
Rust repos service with shipping; pull requests kept in the model519 ));
Merge main (membership, two-factor, GitHub repo roles) into tokens520 };
521 // Who may create which: the workspace's member privileges. A
522 // workspace's own token acts as an owner would.
523 let role = if a.owner.kind == PrincipalKind::Workspace { Role::Owner } else { role };
524 if let Some(why) = a.owner.privileges_in(&namespace).creation_refusal(role, a.is_private, &namespace) {
525 return Ok(Outcome::fail(FailureCode::Forbidden, why));
Rust repos service with shipping; pull requests kept in the model526 }
Workspaces own repositories527 let path = RepoPath { namespace, name };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look528 match self.registry.by_path_any(&path).await? {
529 Some((_, None)) => {
530 return Ok(Outcome::fail(
531 FailureCode::Conflict,
532 "That workspace already has a repository with that name.",
533 ));
534 }
535 Some((_, Some(_))) => {
536 return Ok(Outcome::fail(
537 FailureCode::Conflict,
538 format!(
539 "{}/{} 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.",
540 path.namespace, path.name
541 ),
542 ));
543 }
544 None => {}
545 }
546 // With a credential (a GitHub App installation's token), everything
547 // is copied: every branch and tag. See mirror.rs.
548 let mut credentialed = None;
549 if let (Some(url), Some(token)) = (a.import_url.as_deref(), a.import_token.as_deref()) {
550 let Some(url) = import::clean_url(url) else {
551 return Ok(Outcome::fail(FailureCode::Invalid, "That is not an https repository address."));
552 };
553 let source = mirror::Endpoint::github(&url, token);
554 match mirror::probe(&source).await? {
555 Ok(advertised) => credentialed = Some((source, advertised)),
556 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
557 }
Rust repos service with shipping; pull requests kept in the model558 }
Agents as a team: lifecycle, merge queue, billing and a new shell559 // An import is fetched before anything is created, so that an
560 // address that does not work leaves nothing behind.
561 let mut imported = None;
562 if let Some(url) = a
563 .import_url
564 .as_deref()
565 .map(str::trim)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look566 .filter(|url| !url.is_empty() && credentialed.is_none())
Agents as a team: lifecycle, merge queue, billing and a new shell567 {
568 let Some(url) = import::clean_url(url) else {
569 return Ok(Outcome::fail(
570 FailureCode::Invalid,
571 "Give the https address of a public repository, such as https://github.com/owner/repo.",
572 ));
573 };
574 let remote = match import::discover(&url).await? {
575 Ok(remote) => remote,
576 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
577 };
A public import copies every branch and tag, so an imported library keeps its releases578 imported = Some((remote, url));
Agents as a team: lifecycle, merge queue, billing and a new shell579 }
Rust repos service with shipping; pull requests kept in the model580 let now = now_ms();
581 let repo = Repo {
582 id: new_id("rep", now),
583 namespace: path.namespace,
584 name: path.name,
585 description: a
586 .description
587 .map(|text| text.trim().to_owned())
588 .filter(|text| !text.is_empty()),
589 is_private: a.is_private,
590 owner_id: a.owner.id.clone(),
Agents as a team: lifecycle, merge queue, billing and a new shell591 default_branch: imported
592 .as_ref()
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look593 .map(|(remote, _)| remote.branch.clone())
594 .or_else(|| credentialed.as_ref().and_then(|(_, advertised)| advertised.default_branch()))
595 .unwrap_or_else(|| "main".to_owned()),
Rust repos service with shipping; pull requests kept in the model596 fork_of: None,
Agents as a team: lifecycle, merge queue, billing and a new shell597 protected: false,
RFC 3339 timestamps in identity and repos598 created_at: rfc3339(now),
Search across all of g1t, Explore, and a command palette599 topics: Vec::new(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look600 website: None,
601 archived_at: None,
Rust repos service with shipping; pull requests kept in the model602 };
Merge branch 'worktree-agent-a2013627e5ea4ab13'603 let namespace = match self.place(&repo).await? {
604 Ok(namespace) => namespace,
605 Err(unplaced) => return Ok(Outcome::fail(FailureCode::Conflict, unplaced.message())),
606 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily607 self.registry
608 .claim_store_key(&repo, namespace.as_deref(), &self.store.default_namespace())
609 .await?;
Rust repos service with shipping; pull requests kept in the model610 self.store
611 .create(
612 &store_key(&repo),
613 repo.description.as_deref(),
614 &repo.default_branch,
615 )
616 .await?;
617 self.registry.insert(&repo).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look618 // A repository that was transferred away from this path stops
619 // redirecting here.
620 self.registry
621 .drop_redirect(&RepoPath {
622 namespace: repo.namespace.clone(),
623 name: repo.name.clone(),
624 })
625 .await?;
A public import copies every branch and tag, so an imported library keeps its releases626 // Every branch and tag the import made, announced as pushes.
627 let mut pushed: Vec<(String, String)> = Vec::new();
628 // A public repository, read with no credential: every branch and
629 // tag is copied too, the default branch the one its HEAD names.
630 if let Some((_, url)) = imported {
Agents as a team: lifecycle, merge queue, billing and a new shell631 let access = self
632 .store
633 .open(&store_key(&repo))
634 .await?
635 .access(Scope::Write)
636 .await?;
A public import copies every branch and tag, so an imported library keeps its releases637 let target = mirror::Endpoint::bearer(&access.remote, &access.token);
638 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 200ms639 self.refs_moved(&repo.id).await;
A public import copies every branch and tag, so an imported library keeps its releases640 match copied {
641 Ok(copied) => pushed = mirror::import_pushes(&copied.updated, &repo.default_branch),
642 Err(reason) => {
643 self.registry.remove(&repo.id).await?;
644 return Ok(Outcome::fail(
645 FailureCode::Invalid,
646 format!("The repository could not be stored: {reason}"),
647 ));
648 }
Agents as a team: lifecycle, merge queue, billing and a new shell649 }
650 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look651 if let Some((source, _)) = credentialed {
652 let access = self
653 .store
654 .open(&store_key(&repo))
655 .await?
656 .access(Scope::Write)
657 .await?;
658 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 200ms659 let copied = mirror::copy(&source, &target, mirror::Prune::Yes).await?;
660 self.refs_moved(&repo.id).await;
661 match copied {
A public import copies every branch and tag, so an imported library keeps its releases662 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 look663 Err(reason) => {
664 self.registry.remove(&repo.id).await?;
665 return Ok(Outcome::fail(
666 FailureCode::Invalid,
667 format!("The repository could not be copied: {reason}"),
668 ));
669 }
670 }
671 }
Merge main (membership, two-factor, GitHub repo roles) into tokens672 // Whoever creates a repository is an Admin of it, as a role given
673 // to them on it, whatever the workspace's base permission.
674 if a.owner.kind == PrincipalKind::User
675 && let Some(identity) = &self.identity
676 {
677 let granted: Result<bool> = g1t_kit::call(
678 identity,
679 "grant_creator",
680 &g1t_contracts::members::GrantCreatorArgs {
681 repo_id: repo.id.clone(),
682 namespace: repo.namespace.clone(),
683 name: repo.name.clone(),
684 user_id: a.owner.id.clone(),
685 },
686 )
687 .await;
688 if let Err(error) = granted {
689 worker::console_error!("creator of {} not given Admin: {error}", repo.id);
690 }
691 }
Rust repos service with shipping; pull requests kept in the model692 self.publish(NewEvent {
693 kind: "repo.created",
694 source: SOURCE,
695 repo_id: Some(repo.id.clone()),
696 actor: Some(a.owner.id),
697 data: RepoCreated {
698 repo_id: repo.id.clone(),
699 namespace: repo.namespace.clone(),
700 name: repo.name.clone(),
701 is_private: repo.is_private,
702 },
703 })
704 .await?;
A public import copies every branch and tag, so an imported library keeps its releases705 for (git_ref, head) in &pushed {
706 self.publish_push(&repo, git_ref, None, head, None).await?;
Agents as a team: lifecycle, merge queue, billing and a new shell707 }
Rust repos service with shipping; pull requests kept in the model708 Ok(Outcome::Ok(repo))
709 }
710
Merge branch 'worktree-agent-a2013627e5ea4ab13'711 /// Where a workspace keeps its data, asked of identity only when an EU
712 /// namespace is configured: without one, every workspace's
713 /// repositories go anywhere and identity is never asked.
714 async fn residency_of(&self, workspace: &str) -> Result<shards::Residency> {
715 if self.placement.eu.is_none() {
716 return Ok(shards::Residency::Anywhere);
717 }
718 let Some(identity) = &self.identity else {
719 return Ok(shards::Residency::Anywhere);
720 };
721 let residency: Option<g1t_contracts::identity::DataResidency> = g1t_kit::call(
722 identity,
723 "workspace_residency",
724 &g1t_contracts::identity::SlugArgs { slug: workspace.to_owned() },
725 )
726 .await?;
727 Ok(match residency {
728 Some(g1t_contracts::identity::DataResidency::Eu) => shards::Residency::Eu,
729 _ => shards::Residency::Anywhere,
730 })
731 }
732
733 /// How each bound namespace stands (namespaces.rs).
734 async fn standings(&self) -> Result<Vec<namespaces::Standing>> {
735 let bound = self.store.namespaces();
736 let default = self.store.default_namespace();
737 let now = now_ms();
738 let config = namespaces::Configured {
739 bound: &bound,
740 default: &default,
741 placement: &self.placement,
742 limits: &self.limits,
743 on_fallback: &|namespace| self.store.on_fallback(&shards::compose(Some(namespace), "x", &default)),
744 writable: &|namespace| self.store.writable(namespace),
745 breaker_open: &|namespace| resilience::open_now(namespace, now),
746 };
747 let (held, recent) = futures_util::future::join(namespaces::held(&self.registry.db), namespaces::recent(&self.registry.db, now)).await;
748 Ok(namespaces::standings(&config, &held?, &recent?))
749 }
750
751 /// The namespace a new repository goes in (shards.rs): its workspace's
752 /// residency, then how each namespace stands, read only when there is
753 /// a choice to make. `Ok(None)` for the default.
754 async fn place(&self, repo: &Repo) -> Result<std::result::Result<Option<String>, shards::Unplaced>> {
755 let residency = self.residency_of(&repo.namespace).await?;
756 let bound = self.store.namespaces();
757 let loads = if residency == shards::Residency::Anywhere && !self.placement.needs_loads(&bound) {
758 // One namespace to choose from at most: nothing to read.
759 bound
760 .iter()
761 .map(|namespace| shards::Load {
762 namespace: namespace.clone(),
763 bound: true,
764 writable: self.store.writable(namespace),
765 ..shards::Load::default()
766 })
767 .collect()
768 } else {
769 let default = self.store.default_namespace();
770 let now = now_ms();
771 let config = namespaces::Configured {
772 bound: &bound,
773 default: &default,
774 placement: &self.placement,
775 limits: &self.limits,
776 on_fallback: &|namespace| self.store.on_fallback(&shards::compose(Some(namespace), "x", &default)),
777 writable: &|namespace| self.store.writable(namespace),
778 breaker_open: &|namespace| resilience::open_now(namespace, now),
779 };
780 namespaces::loads(&self.registry.db, &config, now).await?
781 };
782 Ok(self.placement.choose(&repo.id, residency, &loads))
783 }
784
785 /// `storage_options`: what a workspace may choose about where its
786 /// repositories are kept.
787 fn storage_options(&self) -> StorageOptions {
788 let bound = self.store.namespaces();
789 StorageOptions { eu_available: self.placement.eu_available(&bound, |namespace| self.store.writable(namespace)) }
790 }
791
Rust repos service with shipping; pull requests kept in the model792 async fn tree(&self, a: TreeArgs) -> Result<Outcome<TreeView>> {
793 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
794 return Ok(not_found());
795 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily796 let git = self.read_git(&repo).await?;
Rust repos service with shipping; pull requests kept in the model797 let git_ref = a
798 .git_ref
799 .clone()
800 .unwrap_or_else(|| repo.default_branch.clone());
801
802 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
803 // An unknown ref is an error; a repo with no commits is just empty.
804 if a.git_ref.is_some() {
805 return Ok(Outcome::fail(
806 FailureCode::NotFound,
807 "No such branch, tag or commit.",
808 ));
809 }
810 return Ok(Outcome::Ok(TreeView {
811 repo,
812 git_ref,
813 path: a.tree_path,
814 head: None,
815 entries: Vec::new(),
816 readme: None,
817 }));
818 };
819
820 let no_directory = || Outcome::fail(FailureCode::NotFound, "No such directory.");
821 let mut entries = git.read_tree(&head.tree_hash).await?;
822 for segment in a.tree_path.split('/').filter(|segment| !segment.is_empty()) {
823 let next = entries.as_ref().and_then(|entries| {
824 entries
825 .iter()
826 .find(|entry| entry.name == segment && entry.kind == EntryKind::Tree)
827 });
828 let Some(next) = next else {
829 return Ok(no_directory());
830 };
831 entries = git.read_tree(&next.hash).await?;
832 }
833 let Some(mut entries) = entries else {
834 return Ok(no_directory());
835 };
836 // Directories first, then by name.
837 entries.sort_by(|a, b| {
838 (b.kind == EntryKind::Tree)
839 .cmp(&(a.kind == EntryKind::Tree))
840 .then_with(|| a.name.cmp(&b.name))
841 });
842
843 let readme_entry = entries
844 .iter()
845 .find(|entry| entry.kind == EntryKind::Blob && is_readme(&entry.name));
846 let readme = match readme_entry {
847 Some(entry) => git.read_blob(&entry.hash).await?.map(|bytes| Readme {
848 name: entry.name.clone(),
849 text: text_of(bytes),
850 }),
851 None => None,
852 };
853 Ok(Outcome::Ok(TreeView {
854 repo,
855 git_ref,
856 path: a.tree_path,
857 head: Some(head),
858 entries,
859 readme,
860 }))
861 }
862
863 async fn blob(&self, a: BlobArgs) -> Result<Outcome<BlobView>> {
864 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
865 return Ok(not_found());
866 };
867 let bytes = if a.file_path.is_empty() {
868 None
869 } else {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily870 let git = self.read_git(&repo).await?;
Rust repos service with shipping; pull requests kept in the model871 git.read_file(&a.git_ref, &a.file_path).await?
872 };
873 let Some(bytes) = bytes else {
874 return Ok(Outcome::fail(FailureCode::NotFound, "No such file."));
875 };
876 Ok(Outcome::Ok(BlobView {
877 repo,
878 git_ref: a.git_ref,
879 path: a.file_path,
880 size: bytes.len() as u64,
881 text: text_of(bytes),
882 }))
883 }
884
Agents as a team: lifecycle, merge queue, billing and a new shell885 async fn blame(&self, a: BlameArgs) -> Result<Outcome<Blame>> {
886 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
887 return Ok(not_found());
888 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily889 let git = self.read_git(&repo).await?;
Agents as a team: lifecycle, merge queue, billing and a new shell890 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
891 Ok(match blame::blame(&git, &git_ref, &a.file_path).await? {
892 Some(blame) => Outcome::Ok(blame),
893 None => not_found(),
894 })
895 }
896
Rust repos service with shipping; pull requests kept in the model897 async fn log(&self, a: LogArgs) -> Result<Outcome<Vec<Commit>>> {
898 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
899 return Ok(not_found());
900 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily901 let git = self.read_git(&repo).await?;
Rust repos service with shipping; pull requests kept in the model902 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
903 Ok(Outcome::Ok(git.log(&git_ref, a.limit).await?))
904 }
905
Branches and Tags pages, each file's last commit, and the branch menu on files906 /// Which commit last changed each entry of a directory. Kept in this
907 /// colo's cache by repository, head commit and path: a commit's history
908 /// never changes, so an answer is good for as long as it is kept.
909 async fn last_commits(&self, a: g1t_contracts::repos::LastCommitsArgs) -> Result<Outcome<g1t_contracts::repos::LastCommits>> {
910 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
911 return Ok(not_found());
912 };
913 let git = self.read_git(&repo).await?;
914 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
915 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
916 return Ok(Outcome::fail(FailureCode::NotFound, "No such branch, tag or commit."));
917 };
918 let key = format!(
919 "https://last-commits.g1t.internal/{}/{}/{}",
920 repo.id,
921 head.hash,
922 a.tree_path.split('/').map(urlencoding_segment).collect::<Vec<_>>().join("/")
923 );
924 let cache = worker::Cache::default();
925 if let Ok(Some(mut kept)) = cache.get(key.as_str(), false).await {
926 if let Ok(found) = kept.json::<g1t_contracts::repos::LastCommits>().await {
927 return Ok(Outcome::Ok(found));
928 }
929 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait930 // 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 answers931 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 wait932 let budget = a.budget_ms;
933 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 answers934 let (entries, complete) = last_commits::last_commits(&git, &head.hash, &a.tree_path, &out_of_time).await?;
935 let stopped = out_of_time();
Branches and Tags pages, each file's last commit, and the branch menu on files936 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 answers937 if stopped && !found.complete {
938 return Ok(Outcome::Ok(found));
939 }
Branches and Tags pages, each file's last commit, and the branch menu on files940 if let Ok(mut response) = worker::Response::from_json(&found) {
941 let _ = response.headers_mut().set("cache-control", "max-age=604800");
942 let _ = cache.put(key.as_str(), response).await;
943 }
944 Ok(Outcome::Ok(found))
945 }
946
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25947 /// How far each branch head has moved from the default branch's head,
948 /// in one call (drift.rs). Each answer is kept in this colo's cache by
949 /// repository and the pair of hashes, for good: neither history can
950 /// change. A head that moved is the only one walked.
951 async fn branch_drift(&self, a: BranchDriftArgs) -> Result<Outcome<BranchDrifts>> {
952 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
953 return Ok(not_found());
954 };
955 if !store::is_commit_hash(&a.base) {
956 return Ok(Outcome::fail(FailureCode::Invalid, "The default branch's head is a full commit hash."));
957 }
958 let heads: Vec<String> = a.heads.into_iter().take(MAX_DRIFT_HEADS).collect();
959 let git = self.read_git(&repo).await?;
960 let key = |head: &str| format!("https://drift.g1t.internal/{}/{}/{head}", repo.id, a.base);
961 let cache = worker::Cache::default();
962 let (base, kept) = futures_util::future::join(
963 git.log(&a.base, 1),
964 futures_util::future::join_all(heads.iter().map(|head| {
965 let (cache, url) = (&cache, key(head));
966 async move {
967 if !store::is_commit_hash(head) {
968 return None;
969 }
970 let mut found = cache.get(url.as_str(), false).await.ok()??;
971 found.json::<BranchDrift>().await.ok()
972 }
973 })),
974 )
975 .await;
976 let missing: Vec<String> = heads
977 .iter()
978 .zip(&kept)
979 .filter(|(head, kept)| kept.is_none() && store::is_commit_hash(head))
980 .map(|(head, _)| head.clone())
981 .collect();
982 let measured = drift::measure(&git, &a.base, &missing).await;
983 let mut fresh: HashMap<String, BranchDrift> = HashMap::new();
984 for (head, found) in missing.into_iter().zip(measured) {
985 let answer = BranchDrift { head: head.clone(), commit: found.commit, drift: found.drift };
986 if found.settled
987 && let Ok(mut response) = worker::Response::from_json(&answer)
988 {
989 let _ = response.headers_mut().set("cache-control", "public, max-age=31536000, immutable");
990 let _ = cache.put(key(&head).as_str(), response).await;
991 }
992 fresh.insert(head, answer);
993 }
994 let branches = heads
995 .iter()
996 .zip(kept)
997 .map(|(head, kept)| {
998 kept.or_else(|| fresh.get(head).cloned())
999 .unwrap_or_else(|| BranchDrift { head: head.clone(), commit: None, drift: None })
1000 })
1001 .collect();
1002 Ok(Outcome::Ok(BranchDrifts { base: base.ok().and_then(|log| log.into_iter().next()), branches }))
1003 }
1004
Branches and Tags pages, each file's last commit, and the branch menu on files1005 /// The repository's tags, newest commit first, at most 100.
1006 async fn tags(&self, a: g1t_contracts::repos::TagsArgs) -> Result<Outcome<Vec<g1t_contracts::repos::Tag>>> {
1007 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
1008 return Ok(not_found());
1009 };
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 251010 // Kept until the refs move, as the branch list is (store.rs): listing
1011 // the refs is a round trip to the store on every call otherwise.
1012 let version = refs_cache::usable(registry::refs_state(&repo.id), now_ms()).filter(|_| !self.store.on_fallback(&store_key(&repo)));
1013 let kept_at = version.map(|version| format!("https://tags.g1t.internal/{}/{version}", repo.id));
1014 if let Some(url) = &kept_at
1015 && let Ok(Some(mut kept)) = worker::Cache::default().get(url.as_str(), false).await
1016 && let Ok(tags) = kept.json::<Vec<g1t_contracts::repos::Tag>>().await
1017 {
1018 return Ok(Outcome::Ok(tags));
1019 }
1020 let (tags, complete) = self.read_tags(&repo).await?;
1021 if complete
1022 && let Some(url) = &kept_at
1023 && let Ok(mut response) = worker::Response::from_json(&tags)
1024 {
1025 let _ = response.headers_mut().set("cache-control", "public, max-age=300");
1026 let _ = worker::Cache::default().put(url.as_str(), response).await;
1027 }
1028 Ok(Outcome::Ok(tags))
1029 }
1030
1031 /// The tags, and whether every one's commit was read (only then kept).
1032 async fn read_tags(&self, repo: &Repo) -> Result<(Vec<g1t_contracts::repos::Tag>, bool)> {
1033 let git = self.store.open(&store_key(repo)).await?;
Branches and Tags pages, each file's last commit, and the branch menu on files1034 let access = git.access(Scope::Read).await?;
1035 let named: Vec<(String, String)> = refs::heads_and_tags(refs::all(&access).await?)
1036 .into_iter()
1037 .filter_map(|(name, hash)| name.strip_prefix("refs/tags/").map(|tag| (tag.to_owned(), hash)))
1038 .collect();
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 251039 let read = self.read_git(repo).await?;
Branches and Tags pages, each file's last commit, and the branch menu on files1040 let commits = futures_util::future::join_all(named.iter().take(MAX_TAGS_READ).map(|(_, hash)| read.log(hash, 1))).await;
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 251041 let complete = commits.iter().all(Result::is_ok);
Branches and Tags pages, each file's last commit, and the branch menu on files1042 let mut tags: Vec<g1t_contracts::repos::Tag> = named
1043 .into_iter()
1044 .zip(commits.into_iter().map(|found| found.ok().and_then(|list| list.into_iter().next())).chain(std::iter::repeat(None)))
1045 .map(|((name, _), commit)| g1t_contracts::repos::Tag { name, commit })
1046 .collect();
1047 tags.sort_by(|a, b| {
1048 let at = |tag: &g1t_contracts::repos::Tag| tag.commit.as_ref().map(|c| c.authored_at.clone()).unwrap_or_default();
1049 at(b).cmp(&at(a)).then_with(|| b.name.cmp(&a.name))
1050 });
1051 tags.truncate(MAX_TAGS_READ);
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 251052 Ok((tags, complete))
Branches and Tags pages, each file's last commit, and the branch menu on files1053 }
1054
Pull requests from branches1055 /// The repository's branches, default branch first.
1056 async fn branches(&self, a: BranchesArgs) -> Result<Outcome<Vec<Branch>>> {
1057 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
1058 return Ok(not_found());
1059 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1060 let mut branches = self.read_git(&repo).await?.branches().await?;
Pull requests from branches1061 branches.sort_by_key(|branch| branch.name != repo.default_branch);
1062 Ok(Outcome::Ok(branches))
1063 }
1064
Agents as a team: lifecycle, merge queue, billing and a new shell1065 /// Whether a pull request's source lacks commits that the branch it
1066 /// would merge into has.
1067 async fn behind(&self, a: BehindArgs) -> Result<bool> {
1068 let Some(source) = self.registry.by_id(&a.source_id).await? else {
1069 return Ok(false);
1070 };
1071 let target = match &source.fork_of {
1072 Some(id) => self.registry.by_id(id).await?,
1073 None => Some(source.clone()),
1074 };
1075 let Some(target) = target else {
1076 return Ok(false);
1077 };
1078 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 bar1079 let target_branch = a.target_branch.unwrap_or_else(|| target.default_branch.clone());
Agents as a team: lifecycle, merge queue, billing and a new shell1080 let target_head = self
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1081 .read_git(&target)
Agents as a team: lifecycle, merge queue, billing and a new shell1082 .await?
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1083 .log(&target_branch, 1)
Agents as a team: lifecycle, merge queue, billing and a new shell1084 .await?
1085 .into_iter()
1086 .next()
1087 .map(|commit| commit.hash);
1088 let Some(target_head) = target_head else {
1089 return Ok(false);
1090 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1091 let source_git = self.read_git(&source).await?;
Agents as a team: lifecycle, merge queue, billing and a new shell1092 let history = source_git.log(&branch, MAX_ANCESTRY).await?;
1093 if history.is_empty() {
1094 return Ok(false);
1095 }
1096 Ok(!descends_from(&source_git, &history, &target_head).await?)
1097 }
1098
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1099 /// The files a pull request's source and the default branch it would
1100 /// merge into each changed since they last agreed. Where the two lists
1101 /// share no file, the merge cannot conflict; where they do, it may.
1102 async fn divergence(&self, a: BehindArgs) -> Result<Option<Divergence>> {
1103 let Some(source) = self.registry.by_id(&a.source_id).await? else {
1104 return Ok(None);
1105 };
1106 let target = match &source.fork_of {
1107 Some(id) => self.registry.by_id(id).await?,
1108 None => Some(source.clone()),
1109 };
1110 let Some(target) = target else {
1111 return Ok(None);
1112 };
1113 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 bar1114 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 daily1115 let source_git = self.read_git(&source).await?;
1116 let target_git = self.read_git(&target).await?;
1117 // The target's side is the same for every pull request into it, and
1118 // worked out once per head (coalesce.rs).
1119 let (history, side) = futures_util::future::try_join(
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1120 source_git.log(&branch, MAX_ANCESTRY),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1121 self.target_side(&target, &target_git, &target_branch),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1122 )
1123 .await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1124 let target_history = &side.history;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1125 let (Some(head), Some(base)) = (history.first(), target_history.first()) else {
1126 return Ok(None);
1127 };
1128 let behind = !descends_from(&source_git, &history, &base.hash).await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1129 let merge_base = nearest_ancestor_in(&source_git, &history, &side.shared).await?;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1130 let mut divergence = Divergence {
1131 head: head.hash.clone(),
1132 base: base.hash.clone(),
1133 merge_base: merge_base.clone(),
1134 behind,
1135 ..Divergence::default()
1136 };
1137 let merge_base_tree = match &merge_base {
1138 Some(hash) => target_history
1139 .iter()
1140 .find(|commit| commit.hash == *hash)
1141 .map(|commit| commit.tree_hash.clone()),
1142 None => None,
1143 };
1144 let Some(merge_base_tree) = merge_base_tree else {
1145 // No common history to compare from: say nothing is known.
1146 divergence.truncated = true;
1147 return Ok(Some(divergence));
1148 };
1149 let (ours, truncated_ours) =
1150 diff::changed_paths(&source_git, Some(&merge_base_tree), &head.tree_hash).await?;
1151 divergence.ours = ours;
1152 divergence.truncated = truncated_ours;
1153 if behind {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1154 let now = now_ms();
1155 let key = (target.id.clone(), merge_base_tree.clone(), base.tree_hash.clone());
1156 let (theirs, truncated_theirs) = match THEIRS.with(|memo| memo.borrow().get(&key, now)) {
1157 Some(kept) => kept,
1158 None => {
1159 let found = diff::changed_paths(&target_git, Some(&merge_base_tree), &base.tree_hash).await?;
1160 THEIRS.with(|memo| memo.borrow_mut().put(key, found.clone(), now));
1161 found
1162 }
1163 };
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1164 divergence.theirs = theirs;
1165 divergence.truncated |= truncated_theirs;
1166 }
1167 Ok(Some(divergence))
1168 }
1169
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1170 /// A target branch's history from its head, worked out once per head
1171 /// for every pull request asking about it (coalesce.rs). The head is
1172 /// read under the refs version; the history by its hash, which the
1173 /// object cache keeps for good.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1174 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 daily1175 let now = now_ms();
1176 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 bar1177 .map(|version| (target.id.clone(), branch.to_owned(), version));
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1178 if let Some(key) = &key
1179 && let Some(side) = TARGETS.with(|memo| memo.borrow().get(key, now))
1180 {
1181 return Ok(side);
1182 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1183 let history = match git.log(branch, 1).await?.first() {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1184 Some(head) => git.log(&head.hash, MAX_ANCESTRY).await?,
1185 None => Vec::new(),
1186 };
1187 let side = coalesce::TargetSide::new(history);
1188 if let Some(key) = key {
1189 TARGETS.with(|memo| memo.borrow_mut().put(key, side.clone(), now));
1190 }
1191 Ok(side)
1192 }
1193
Pull requests from branches1194 async fn head(&self, a: HeadArgs) -> Result<Option<String>> {
1195 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
1196 return Ok(None);
1197 };
Workflows run when an agent's pull request is marked ready1198 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 daily1199 let git = self.read_git(&repo).await?;
Pull requests from branches1200 Ok(git
Workflows run when an agent's pull request is marked ready1201 .log(branch, 1)
Pull requests from branches1202 .await?
1203 .into_iter()
1204 .next()
1205 .map(|commit| commit.hash))
1206 }
1207
Merge queue: tested states are deleted once their entry leaves1208 async fn delete_branch(&self, a: DeleteBranchArgs) -> Result<Outcome<bool>> {
1209 if !a.branch.starts_with(G1T_BRANCH_PREFIX) {
1210 return Ok(Outcome::fail(
1211 FailureCode::Forbidden,
1212 "Only branches g1t made for itself can be deleted this way.",
1213 ));
1214 }
1215 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
1216 return Ok(not_found());
1217 };
Merge branch 'worktree-agent-a2013627e5ea4ab13'1218 let repo = match self.unpaused(repo).await? {
1219 Ok(repo) => repo,
1220 Err((code, message)) => return Ok(Outcome::fail(code, message)),
1221 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1222 self.live(&repo).await?;
Merge queue: tested states are deleted once their entry leaves1223 let git = self.store.open(&store_key(&repo)).await?;
1224 let Some(old) = git
1225 .branches()
1226 .await?
1227 .into_iter()
1228 .find(|branch| branch.name == a.branch)
1229 .map(|branch| branch.hash)
1230 else {
1231 return Ok(Outcome::Ok(false));
1232 };
1233 let access = git.access(Scope::Write).await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1234 let deleted = land::delete_ref(&access, &a.branch, &old).await?;
1235 self.refs_moved(&repo.id).await;
1236 if let Err(reason) = deleted {
Merge queue: tested states are deleted once their entry leaves1237 return Ok(Outcome::fail(
1238 FailureCode::Conflict,
1239 format!("{} could not be deleted: {reason}", a.branch),
1240 ));
1241 }
1242 Ok(Outcome::Ok(true))
1243 }
1244
Issues and pull requests replace intents and attempts1245 async fn fork_for_pull(&self, a: ForkArgs) -> Result<Outcome<Repo>> {
Rust repos service with shipping; pull requests kept in the model1246 let viewer = Some(a.actor.clone());
1247 let Some(source) = self
1248 .registry
1249 .by_id(&a.source_id)
1250 .await?
1251 .filter(|repo| can_read(repo, &viewer))
1252 else {
1253 return Ok(not_found());
1254 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1255 if let Some((code, message)) = lifecycle::archived_refusal(&source) {
1256 return Ok(Outcome::fail(code, message));
1257 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'1258 // Its working copy is made in its namespace: not while it moves.
1259 let source = match self.unpaused(source).await? {
1260 Ok(source) => source,
1261 Err((code, message)) => return Ok(Outcome::fail(code, message)),
1262 };
Rust repos service with shipping; pull requests kept in the model1263 let now = now_ms();
1264 let fork = Repo {
1265 id: new_id("rep", now),
Issues and pull requests replace intents and attempts1266 namespace: PULLS_NAMESPACE.to_owned(),
1267 name: a.pull_id.clone(),
Rust repos service with shipping; pull requests kept in the model1268 description: None,
1269 // A fork is exactly as visible as the repo it came from.
1270 is_private: source.is_private,
1271 owner_id: a.actor.id.clone(),
1272 default_branch: source.default_branch.clone(),
1273 fork_of: Some(source.id.clone()),
Agents as a team: lifecycle, merge queue, billing and a new shell1274 protected: false,
RFC 3339 timestamps in identity and repos1275 created_at: rfc3339(now),
Search across all of g1t, Explore, and a command palette1276 topics: Vec::new(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1277 website: None,
1278 archived_at: None,
Rust repos service with shipping; pull requests kept in the model1279 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1280 // Artifacts forks within a namespace: the copy goes where its
1281 // repository is.
1282 let (namespace, _) = store::locate(&store_key(&source));
1283 self.registry
1284 .claim_store_key(&fork, Some(&namespace), &self.store.default_namespace())
1285 .await?;
Rust repos service with shipping; pull requests kept in the model1286 self.store
1287 .open(&store_key(&source))
1288 .await?
1289 .fork(&store_key(&fork))
1290 .await?;
1291 self.registry.insert(&fork).await?;
1292 self.publish(NewEvent {
1293 kind: "repo.forked",
1294 source: SOURCE,
1295 repo_id: Some(source.id.clone()),
1296 actor: Some(a.actor.id),
1297 data: RepoForked {
1298 repo_id: fork.id.clone(),
1299 source_repo_id: source.id,
Issues and pull requests replace intents and attempts1300 pull_id: a.pull_id,
Rust repos service with shipping; pull requests kept in the model1301 },
1302 })
1303 .await?;
1304 Ok(Outcome::Ok(fork))
1305 }
1306
1307 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 200ms1308 let found = self.registry.by_path(&a.path).await?;
1309 Ok(match self.authorize_git(&a.path, &a.viewer, a.service, found).await? {
1310 Outcome::Ok(repo) => {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1311 self.live(&repo).await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1312 let write = a.service == GitService::ReceivePack;
1313 if write {
1314 // A push with this credential would not pass through
1315 // here, so nothing that lists the refs is kept until it
1316 // has expired (see refs_cache.rs).
1317 let until = now_ms() + store::CREDENTIAL_LIFE_MS + 60_000;
1318 if let Err(error) = self.registry.refs_open(&repo.id, until).await {
1319 // Before the column exists nothing is kept anyway.
1320 if registry::refs_state(&repo.id).is_some() {
1321 return Err(error);
1322 }
1323 }
1324 }
1325 let scope = if write { Scope::Write } else { Scope::Read };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1326 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 200ms1327 }
1328 Outcome::Fail(failure) => Outcome::Fail(failure),
1329 })
1330 }
1331
1332 /// The repository at `path` (`found`, as just read), if the viewer may
1333 /// use `service` on it: fetch from it, or push to it. A push to a path
1334 /// with nothing there makes the repository, in a workspace the pusher
1335 /// belongs to.
1336 async fn authorize_git(
1337 &self,
1338 path: &RepoPath,
1339 viewer: &Viewer,
1340 service: GitService,
1341 found: Option<Repo>,
1342 ) -> Result<Outcome<Repo>> {
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1343 let mut a = GitAccessArgs {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1344 path: path.clone(),
1345 viewer: viewer.clone(),
1346 service,
1347 };
Rust repos service with shipping; pull requests kept in the model1348 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 step1349 // An access token: pushing needs code:write, reading a private
1350 // repository code:read. A public repository reads as it would for
1351 // anyone. Which repositories a token reaches is its owner's, checked
1352 // below as for anyone.
1353 if let Some(access) = a.viewer.as_ref().and_then(|user| user.token.as_deref()).cloned() {
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1354 // A workflow job's token, and a deploy key, reach their own
1355 // repository only; a job's also the working copies of that
1356 // repository's pull requests, where their heads are.
1357 let name = format!("{}/{}", path.namespace, path.name);
1358 let source = match found.as_ref().and_then(|repo| repo.fork_of.as_deref()) {
1359 Some(source_id) if g1t_contracts::scopes::decide_repo(&access, &name).is_some() => self
1360 .registry
1361 .by_id(source_id)
1362 .await?
1363 .map(|source| format!("{}/{}", source.namespace, source.name)),
1364 _ => None,
1365 };
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1366 let public = found.as_ref().is_some_and(|repo| !repo.is_private);
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1367 if let Some(why) = git_token_refusal(&access, &name, source.as_deref(), write, public, found.is_some()) {
1368 return Ok(Outcome::fail(FailureCode::Forbidden, format!("{why}\n")));
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1369 }
1370 if !write && !access.allows(g1t_contracts::scopes::Scope::CodeRead) {
1371 a.viewer = None;
1372 }
1373 }
1374
Rust repos service with shipping; pull requests kept in the model1375 // Anonymous callers are asked to authenticate whether or not the repo
1376 // exists, so private repos cannot be told apart from missing ones.
1377 let denied = || match &a.viewer {
1378 Some(_) => not_found(),
1379 None => Outcome::fail(FailureCode::Unauthenticated, "Authentication required."),
1380 };
Agents as a team: lifecycle, merge queue, billing and a new shell1381 // An agent's token works through the API only: its sandbox has its
1382 // own way to push, to its own pull request.
1383 if a.viewer.as_ref().is_some_and(|user| user.kind == PrincipalKind::Agent) {
1384 return Ok(Outcome::fail(
1385 FailureCode::Forbidden,
1386 "A g1t agent's token cannot be used with git.",
1387 ));
1388 }
Rust repos service with shipping; pull requests kept in the model1389 if let (true, Some(user)) = (write, &a.viewer)
1390 && !user.verified
1391 {
1392 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
1393 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1394 let repo = match found {
Rust repos service with shipping; pull requests kept in the model1395 Some(repo) => {
1396 let allowed = if write {
1397 can_write(&repo, &a.viewer)
1398 } else {
Members can read a private repository's pull request forks1399 self.may_read(&repo, &a.viewer).await?
Rust repos service with shipping; pull requests kept in the model1400 };
1401 if !allowed {
1402 return Ok(denied());
1403 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1404 // An archived repository, or a pull request's copy of one,
1405 // is read-only.
1406 if write {
1407 let archived = match &repo.fork_of {
1408 Some(source) => self.registry.by_id(source).await?,
1409 None => Some(repo.clone()),
1410 };
1411 match archived {
1412 Some(source) => {
1413 if let Some((code, message)) = lifecycle::archived_refusal(&source) {
1414 return Ok(Outcome::fail(code, format!("{message}\n")));
1415 }
1416 }
1417 // The repository it was copied from is deleted.
1418 None => return Ok(denied()),
1419 }
1420 }
Rust repos service with shipping; pull requests kept in the model1421 repo
1422 }
1423 None => {
Workspaces own repositories1424 // Push to create, in a workspace the pusher belongs to.
Rust repos service with shipping; pull requests kept in the model1425 let owner = a
1426 .viewer
1427 .as_ref()
Workspaces own repositories1428 .filter(|user| write && user.is_member(&a.path.namespace.to_lowercase()));
Rust repos service with shipping; pull requests kept in the model1429 let Some(owner) = owner else {
1430 return Ok(denied());
1431 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1432 let created = self.create(push_to_create(owner, &a.path)).await?;
Rust repos service with shipping; pull requests kept in the model1433 match created {
1434 Outcome::Ok(repo) => repo,
1435 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
1436 }
1437 }
1438 };
Merge branch 'worktree-agent-a2013627e5ea4ab13'1439 // A push, or a credential to push with, waits while the repository
1440 // moves between namespaces (moves.rs), and goes to where it is now.
1441 if write {
1442 return Ok(match self.unpaused(repo).await? {
1443 Ok(repo) => Outcome::Ok(repo),
1444 Err((code, message)) => Outcome::fail(code, format!("{message}\n")),
1445 });
1446 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1447 Ok(Outcome::Ok(repo))
Rust repos service with shipping; pull requests kept in the model1448 }
1449
1450 async fn land(&self, a: LandArgs) -> Result<Outcome<Landed>> {
1451 let actor: Viewer = Some(a.actor.clone());
Pull requests from branches1452 let Some(source) = self.registry.by_id(&a.source_id).await? else {
Rust repos service with shipping; pull requests kept in the model1453 return Ok(not_found());
1454 };
Pull requests from branches1455 // A fork lands on the repository it came from; a branch on its own.
1456 let target = match &source.fork_of {
Rust repos service with shipping; pull requests kept in the model1457 Some(id) => self.registry.by_id(id).await?,
Pull requests from branches1458 None => Some(source.clone()),
Rust repos service with shipping; pull requests kept in the model1459 };
1460 let Some(target) = target.filter(|repo| can_read(repo, &actor)) else {
1461 return Ok(not_found());
1462 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1463 if !registry::can(&target, &actor, Capability::Merge) {
Rust repos service with shipping; pull requests kept in the model1464 return Ok(Outcome::fail(
1465 FailureCode::Forbidden,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1466 access::needs(Capability::Merge, &format!("{}/{}", target.namespace, target.name)),
Rust repos service with shipping; pull requests kept in the model1467 ));
1468 }
1469 if !a.actor.verified {
1470 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
1471 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1472 if let Some((code, message)) = lifecycle::archived_refusal(&target) {
1473 return Ok(Outcome::fail(code, message));
1474 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'1475 // Moving between namespaces: wait for it (moves.rs). Both are read
1476 // again once it is done, for their new keys.
1477 let (source, target) = match (self.unpaused(source).await?, self.unpaused(target).await?) {
1478 (Ok(source), Ok(target)) => (source, target),
1479 (Err((code, message)), _) | (_, Err((code, message))) => return Ok(Outcome::fail(code, message)),
1480 };
Rust repos service with shipping; pull requests kept in the model1481
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1482 let branch = &a.target_branch.clone().unwrap_or_else(|| target.default_branch.clone());
Pull requests from branches1483 let from_fork = source.id != target.id;
1484 let source_branch = match a.branch {
1485 Some(name) if !from_fork && name == *branch => {
1486 return Ok(Outcome::fail(
1487 FailureCode::Invalid,
1488 format!("{branch} cannot be merged into itself."),
1489 ));
1490 }
1491 Some(name) => name,
1492 None if from_fork => branch.clone(),
1493 None => {
1494 return Ok(Outcome::fail(
1495 FailureCode::Invalid,
1496 "Say which branch to merge.",
1497 ));
1498 }
1499 };
1500
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1501 self.live(&source).await?;
Pull requests from branches1502 let source_git = self.store.open(&store_key(&source)).await?;
Rust repos service with shipping; pull requests kept in the model1503 let target_git = self.store.open(&store_key(&target)).await?;
Pull requests from branches1504 let history = source_git.log(&source_branch, MAX_ANCESTRY).await?;
Rust repos service with shipping; pull requests kept in the model1505 let Some(new) = history.first().map(|commit| commit.hash.clone()) else {
1506 return Ok(Outcome::fail(
1507 FailureCode::Conflict,
Issues and pull requests replace intents and attempts1508 "This pull request has no commits to merge.",
Rust repos service with shipping; pull requests kept in the model1509 ));
1510 };
1511 let old = target_git
1512 .log(branch, 1)
1513 .await?
1514 .into_iter()
1515 .next()
1516 .map(|commit| commit.hash);
1517
1518 if old.as_deref() == Some(new.as_str()) {
Diffs on attempts; hosted agent presented as the g1t agent1519 return Ok(Outcome::Ok(Landed {
1520 commit: new,
1521 previous: None,
1522 }));
Rust repos service with shipping; pull requests kept in the model1523 }
1524 // Moving the branch to a commit that does not descend from its
1525 // current head would discard whatever landed in between.
1526 if let Some(old) = &old
Pull requests from branches1527 && !descends_from(&source_git, &history, old).await?
Rust repos service with shipping; pull requests kept in the model1528 {
Pull requests from branches1529 let remedy = if from_fork {
1530 format!("Pull {branch} into the pull request's fork, push, and merge again.")
1531 } else {
1532 format!("Merge {branch} into {source_branch}, push, and merge again.")
1533 };
Rust repos service with shipping; pull requests kept in the model1534 return Ok(Outcome::fail(
1535 FailureCode::Conflict,
Pull requests from branches1536 format!("{branch} has moved since this pull request was opened. {remedy}"),
Rust repos service with shipping; pull requests kept in the model1537 ));
1538 }
1539
Pull requests from branches1540 // For a branch the objects are already in the target; sending them
1541 // again is harmless and keeps one way of moving a ref.
1542 let source_access = source_git.access(Scope::Read).await?;
Rust repos service with shipping; pull requests kept in the model1543 let target_access = target_git.access(Scope::Write).await?;
1544 let pushed =
1545 land::fast_forward(&source_access, &target_access, branch, old.as_deref(), &new)
1546 .await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1547 self.refs_moved(&target.id).await;
Rust repos service with shipping; pull requests kept in the model1548 if let Err(reason) = pushed {
Issues and pull requests replace intents and attempts1549 // Most often another pull request landed between the check and the push.
Rust repos service with shipping; pull requests kept in the model1550 return Ok(Outcome::fail(
1551 FailureCode::Conflict,
1552 format!("{branch} could not be updated: {reason}"),
1553 ));
1554 }
GitHub Actions on g1t, part one: reading workflows1555 self.publish_push(
1556 &target,
1557 &format!("refs/heads/{branch}"),
1558 old.as_deref(),
1559 &new,
Merge branch 'worktree-agent-a3abfcce648e87dca'1560 Some(&a.actor),
GitHub Actions on g1t, part one: reading workflows1561 )
Events service in Rust, with RFC 3339 times and accurate push events1562 .await?;
Diffs on attempts; hosted agent presented as the g1t agent1563 Ok(Outcome::Ok(Landed {
1564 commit: new,
1565 previous: old,
1566 }))
1567 }
1568
1569 async fn compare(&self, a: CompareArgs) -> Result<Outcome<Comparison>> {
1570 let Some(repo) = self
Members can read a private repository's pull request forks1571 .visible(self.registry.by_id(&a.repo_id).await?, &a.viewer)
Diffs on attempts; hosted agent presented as the g1t agent1572 .await?
1573 else {
1574 return Ok(not_found());
1575 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1576 let git = self.read_git(&repo).await?;
Pull requests from branches1577 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 bar1578 // A pull request into another branch is compared from where it
1579 // left that branch.
1580 let base_branch = a.base_branch.clone();
Agents as a team: lifecycle, merge queue, billing and a new shell1581 // The head's history is only searched when the base is worked out
1582 // from another branch.
1583 let depth = if a.base.is_some() || is_commit_hash(head_ref) { 1 } else { MAX_ANCESTRY };
1584 let history = git.log(head_ref, depth).await?;
Diffs on attempts; hosted agent presented as the g1t agent1585 let Some(head) = history.first() else {
1586 return Ok(Outcome::fail(
1587 FailureCode::Conflict,
Pull requests from branches1588 "There are no commits to compare.",
Diffs on attempts; hosted agent presented as the g1t agent1589 ));
1590 };
1591
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1592 // Where the head's history meets the default branch of `against`,
1593 // or the branch asked for.
Pull requests from branches1594 let shared_with = async |against: &Repo| -> Result<Option<String>> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1595 let against_git = self.read_git(against).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1596 let branch = base_branch.as_deref().unwrap_or(&against.default_branch);
Pull requests from branches1597 let shared: HashSet<String> = against_git
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1598 .log(branch, MAX_ANCESTRY)
Pull requests from branches1599 .await?
1600 .into_iter()
1601 .map(|commit| commit.hash)
1602 .collect();
1603 nearest_ancestor_in(&git, &history, &shared).await
1604 };
Diffs on attempts; hosted agent presented as the g1t agent1605 let base = match (a.base, &repo.fork_of) {
1606 (Some(base), _) => Some(base),
1607 // A fork is compared with the last commit it shares with the
1608 // repository it came from.
1609 (None, Some(target_id)) => match self.registry.by_id(target_id).await? {
Pull requests from branches1610 Some(target) => shared_with(&target).await?,
Diffs on attempts; hosted agent presented as the g1t agent1611 None => None,
1612 },
Pull requests from branches1613 // A branch, with the point where it left the default branch.
Agents as a team: lifecycle, merge queue, billing and a new shell1614 // A single commit, with its first parent.
1615 (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 bar1616 (None, None) if head_ref != base_branch.as_deref().unwrap_or(&repo.default_branch) => {
1617 shared_with(&repo).await?
1618 }
Diffs on attempts; hosted agent presented as the g1t agent1619 (None, None) => head.parents.first().cloned(),
1620 };
1621 let base_tree = match &base {
1622 Some(base) => git
1623 .log(base, 1)
1624 .await?
1625 .into_iter()
1626 .next()
1627 .map(|commit| commit.tree_hash),
1628 None => None,
1629 };
1630 let (files, truncated) =
1631 diff::compare_trees(&git, base_tree.as_deref(), &head.tree_hash).await?;
1632 Ok(Outcome::Ok(Comparison {
1633 base,
1634 head: head.hash.clone(),
1635 files,
1636 truncated,
1637 }))
Rust repos service with shipping; pull requests kept in the model1638 }
1639
Merge branch 'worktree-agent-a3abfcce648e87dca'1640 /// Reports that `git_ref` of `repo` (a full ref) now points to `after`,
1641 /// moved by `actor` (marked when that was a workflow job's token).
Events service in Rust, with RFC 3339 times and accurate push events1642 async fn publish_push(
1643 &self,
1644 repo: &Repo,
GitHub Actions on g1t, part one: reading workflows1645 git_ref: &str,
1646 before: Option<&str>,
Events service in Rust, with RFC 3339 times and accurate push events1647 after: &str,
Merge branch 'worktree-agent-a3abfcce648e87dca'1648 actor: Option<&User>,
Events service in Rust, with RFC 3339 times and accurate push events1649 ) -> Result<()> {
Merge branch 'worktree-agent-a3abfcce648e87dca'1650 let caused_by_job = actor.and_then(g1t_contracts::events::job_run_of).map(str::to_owned);
1651 self.publish_git_push(repo, git_ref, before, after, actor.map(|user| user.id.clone()), false, caused_by_job).await
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1652 }
1653
1654 /// `publish_push`, saying whether the push reached the store without
1655 /// being scanned for secrets first.
Merge branch 'worktree-agent-a3abfcce648e87dca'1656 #[allow(clippy::too_many_arguments)]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1657 async fn publish_git_push(
1658 &self,
1659 repo: &Repo,
1660 git_ref: &str,
1661 before: Option<&str>,
1662 after: &str,
1663 actor: Option<String>,
1664 unscanned: bool,
Merge branch 'worktree-agent-a3abfcce648e87dca'1665 caused_by_job: Option<String>,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1666 ) -> Result<()> {
Rust repos service with shipping; pull requests kept in the model1667 self.publish(NewEvent {
1668 kind: "git.push",
1669 source: SOURCE,
1670 repo_id: Some(repo.id.clone()),
1671 actor,
1672 data: GitPush {
1673 repo_id: repo.id.clone(),
GitHub Actions on g1t, part one: reading workflows1674 git_ref: git_ref.to_owned(),
1675 before: before.map(str::to_owned),
Rust repos service with shipping; pull requests kept in the model1676 after: after.to_owned(),
GitHub Actions on g1t, part one: reading workflows1677 default_branch: git_ref.strip_prefix("refs/heads/")
1678 == Some(repo.default_branch.as_str()),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1679 unscanned,
Merge branch 'worktree-agent-a3abfcce648e87dca'1680 caused_by_job,
Rust repos service with shipping; pull requests kept in the model1681 },
1682 })
1683 .await
1684 }
1685
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1686 /// Git over HTTPS. Only what decides the answer happens before it:
1687 /// the repository, who is asking and whether they may, the free
1688 /// workspace limits, push protection, and the store's own answer. The
1689 /// audit entry and what a push changed are recorded once git has its
1690 /// answer. Each answer says how long its steps took (`Server-Timing`).
1691 async fn git_http(&self, request: Request, env: &Env, ctx: &Context) -> Result<Response> {
1692 let mut timing = git_http::Timing::start();
Rust repos service with shipping; pull requests kept in the model1693 let Some(git) = git_http::parse(&request.url()?) else {
1694 return Response::error("Not found", 404);
1695 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1696 let response = match self.answer_git(request, &git, env, ctx, &mut timing).await {
1697 Ok(response) => response,
1698 // The git store is busy: git hears when to try again.
1699 Err(error) => match resilience::busy(&error.to_string()) {
1700 Some(busy) => git_http::busy_response(busy)?,
1701 None => return Err(error),
1702 },
1703 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1704 timing.apply(response)
1705 }
1706
1707 async fn answer_git(
1708 &self,
1709 request: Request,
1710 git: &git_http::GitRequest,
1711 env: &Env,
1712 ctx: &Context,
1713 timing: &mut git_http::Timing,
1714 ) -> Result<Response> {
1715 let write = git.service == GitService::ReceivePack;
1716 let get = request.method() == Method::Get;
1717 let identity = env.service("IDENTITY")?;
1718 // The repository and the caller's credentials, at once. A fetch may
1719 // go by the row as read a moment ago, for the same clone's next
1720 // request; a push always reads it. Anonymous callers cost nothing.
1721 let lookup = async {
1722 if write {
1723 self.registry.by_path(&git.path).await
1724 } else {
1725 self.registry.by_path_recent(&git.path).await
1726 }
1727 };
1728 let (found, viewer) =
1729 futures_util::future::join(lookup, git_http::viewer(&request, &identity)).await;
Merge branch 'worktree-agent-a8385d293d42c913a'1730 let mut found = found?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1731 timing.mark("repo");
Merge branch 'worktree-agent-a8385d293d42c913a'1732 // A workspace alias staff set (identity's aliases.rs: `g1t` for
1733 // `flagon-io`) is answered in place, as the repository under the
1734 // workspace's slug: pushes and some clients do not follow
1735 // redirects. Everything after this sees only the workspace's slug.
1736 let aliased = match found {
1737 Some(_) => None,
1738 None => git_http::aliased(git, &identity).await?,
1739 };
1740 if let Some(aliased) = &aliased {
1741 found = if write {
1742 self.registry.by_path(&aliased.path).await?
1743 } else {
1744 self.registry.by_path_recent(&aliased.path).await?
1745 };
1746 timing.mark("alias");
1747 }
1748 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 200ms1749 if found.is_none() {
1750 // A workspace that was renamed: git follows a redirect when it
1751 // first asks for refs, and uses the new address from then on.
1752 // A repository transferred to another workspace: the same, to
1753 // its new path. Fetches and pushes both follow either.
1754 let url = request.url()?;
1755 let (renamed, moved) = futures_util::future::join(
1756 git_http::renamed(&url, &identity),
1757 self.registry.resolve_moved(&git.path),
1758 )
1759 .await;
1760 timing.mark("moved");
1761 if let Some(location) = renamed? {
1762 return git_http::moved(&location, get);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1763 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1764 if let Some(now) = moved?
1765 && let Some(location) = git_http::transferred(&url, &now)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1766 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1767 return git_http::moved(&location, get);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1768 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1769 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1770 let viewer = viewer?;
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)1771 // A person who has not confirmed their email address can do
1772 // nothing with git until they do: told so, not asked to sign in.
1773 if viewer.as_ref().is_some_and(g1t_contracts::User::awaits_confirmation) {
1774 let site = request.url()?.origin().ascii_serialization();
1775 return git_http::refuse(Outcome::<()>::fail(
1776 FailureCode::Forbidden,
1777 g1t_contracts::accounts::confirm_email_first(&site),
1778 ));
1779 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1780 // A run credential is checked against its grants, then acts as the
1781 // 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 200ms1782 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 API1783 run_access::Admitted::Go { request, viewer, entry } => (request, viewer, entry),
1784 run_access::Admitted::Refused(response) => return Ok(response),
1785 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1786 let mut after = AfterGit {
1787 audit,
1788 status: 0,
1789 message: None,
1790 push: None,
1791 };
1792 let repo = match self.authorize_git(&git.path, &viewer, git.service, found).await? {
1793 Outcome::Ok(repo) => repo,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1794 refused => {
1795 let response = git_http::refuse(refused)?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1796 after.ended(response.status_code(), None);
1797 after.spawn(env, ctx);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1798 return Ok(response);
1799 }
Rust repos service with shipping; pull requests kept in the model1800 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1801 // A pull request's working copy removed after it closed is made
1802 // again before git uses it (forks.rs).
1803 self.live(&repo).await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1804 timing.mark("access");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1805 // Clones check out the default branch g1t keeps, which can have
1806 // changed since the store made the repository.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1807 let default_branch = repo.fork_of.is_none().then(|| repo.default_branch.clone());
1808 let key = store_key(&repo);
1809 let scope = if write { Scope::Write } else { Scope::Read };
1810 let mut request = request;
1811 let protocol = refs_cache::protocol(request.headers().get("git-protocol")?.as_deref());
1812 // A fetch's POST is read here, to tell an `ls-refs` from a fetch of
1813 // objects; the store would have it read in full anyway.
1814 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 daily1815 // What it asks the store, for the meters (meters.rs).
1816 let call = git_ops::classify(git.service, git.endpoint, get, body.as_deref());
Merge branch 'worktree-agent-a2013627e5ea4ab13'1817 // Answers kept from the usual store may name refs the fallback
1818 // store does not have (fallback.rs): none are used, or kept.
1819 let fallback = self.store.on_fallback(&key);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1820 // An answer that lists refs may have been kept: see refs_cache.rs.
1821 let kept_key = refs_cache::kind(git, get, protocol, body.as_deref())
Merge branch 'worktree-agent-a2013627e5ea4ab13'1822 .filter(|_| !fallback)
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1823 .zip(refs_cache::usable(registry::refs_state(&repo.id), now_ms()))
1824 .map(|(kind, version)| {
1825 refs_cache::Key::new(&repo.id, version, default_branch.as_deref(), protocol, &kind)
1826 });
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1827 // A fresh clone's pack may have been kept too: see pack_cache.rs.
1828 // Under the same refs version, so never across a change to them.
1829 let pack_key = self
1830 .packs
1831 .as_ref()
Merge branch 'worktree-agent-a2013627e5ea4ab13'1832 .filter(|_| !fallback)
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1833 .and_then(|_| {
1834 let encoding = request.headers().get("content-encoding").ok().flatten();
1835 pack_cache::cacheable(git, get, protocol, encoding.as_deref(), body.as_deref())
1836 })
1837 .zip(refs_cache::usable(registry::refs_state(&repo.id), now_ms()))
1838 .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 200ms1839 // A kept answer and the free workspace limits, with a kept
1840 // credential looked up alongside. A kept answer goes back without
1841 // waiting for the credential, which it does not need.
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1842 let ((answer, pack, limited), kept_access) = {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1843 let shared = self.shared.as_deref();
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1844 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 200ms1845 async {
1846 match &kept_key {
1847 Some(kept_key) => refs_cache::get(shared, kept_key).await,
1848 None => None,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1849 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1850 },
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1851 async {
1852 match (&pack_key, self.packs.as_deref()) {
1853 (Some(pack_key), Some(packs)) => pack_cache::get(packs, pack_key).await,
1854 _ => None,
1855 }
1856 },
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1857 self.git_limits(call, git, &repo, env),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1858 ));
1859 let kept_access = std::pin::pin!(self.store.kept_access(&key, scope));
1860 match futures_util::future::select(answer_and_limits, kept_access).await {
1861 futures_util::future::Either::Left((first, kept_access)) => {
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1862 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 200ms1863 (first, if answered { None } else { kept_access.await })
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1864 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1865 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 look1866 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1867 };
1868 timing.mark("kept");
A clone answered from the pack cache is served before the free operation cap: it is not an operation1869 // A kept pack first: it never reaches the store, so it is never an
1870 // operation, and a free workspace past its operation cap still gets
1871 // it. (The other limits are a push's, and a pack is only a fetch.)
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1872 if let Some(kept) = pack {
1873 timing.note("pack", "hit");
1874 let sent = body.as_ref().map_or(0, |body| body.len() as u64);
1875 meters::record(pack_cache::HIT, &key, sent, kept.size);
1876 after.ended(200, None);
1877 after.spawn(env, ctx);
1878 return kept.response();
1879 }
A clone answered from the pack cache is served before the free operation cap: it is not an operation1880 if let Some((response, status, message)) = limited? {
1881 after.ended(status, Some(message.to_owned()));
1882 after.spawn(env, ctx);
1883 return Ok(response);
1884 }
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1885 if pack_key.is_some() {
1886 timing.note("pack", "miss");
1887 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1888 if let (Some((entry, found)), Some(kept_key)) = (answer, &kept_key) {
1889 timing.note("refs", found.as_str());
1890 if found == refs_cache::Found::Shared {
1891 let (kept_key, entry) = (kept_key.clone(), entry.clone());
1892 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 look1893 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1894 // Never reached the store: never an operation.
1895 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 200ms1896 after.ended(200, None);
1897 after.spawn(env, ctx);
1898 return entry.response();
1899 }
1900 if kept_key.is_some() {
1901 timing.note("refs", "miss");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1902 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1903 // The store's credential: one made a moment ago, here or in another
1904 // isolate (see store.rs), or a new one.
1905 let access = match kept_access {
1906 Some((access, from)) => {
1907 timing.note("cred", from.as_str());
1908 access
1909 }
1910 None => {
1911 let access = self.store.mint_access(&key, scope).await?;
1912 timing.mark("mint");
1913 timing.note("cred", "mint");
1914 access
1915 }
1916 };
1917 // Should the store turn a kept credential down, a fetch's first
1918 // request is tried again with a new one; the requests after it then
1919 // have that one too.
1920 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 API1921 // 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 200ms1922 let scan = async |body: &[u8]| self.protect(&repo, viewer.as_ref(), body).await;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1923 // Rulesets: what the rules of the branches and tags it changes
1924 // refuse is declined, saying which rule and why (rules.rs).
1925 let rules = async |head: &[u8], whole: bool| self.check_push(&repo, viewer.as_ref(), head, whole).await;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1926 // What a push may bring (pack_limits.rs): the repository's size is
1927 // its own and its pull requests' working copies'.
1928 let limits = if write && !get {
1929 git_http::PushLimits {
1930 held: self.held(&repo).await,
1931 repo_limit: self.repo_limit,
1932 large: self.large_pushes,
1933 ..git_http::PushLimits::default()
1934 }
1935 } else {
1936 git_http::PushLimits::default()
1937 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1938 let mut outcome = git_http::forward(
1939 request,
1940 body,
1941 git,
1942 &access,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1943 rules,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1944 default_branch.as_deref(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1945 limits,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1946 scan,
1947 )
1948 .await?;
1949 let turned_down = matches!(
1950 &outcome,
1951 git_http::Push::Forwarded(forwarded) if matches!(forwarded.response.status_code(), 401 | 403)
1952 );
1953 if turned_down {
1954 self.store.forget_access(&key).await;
1955 if let Some(again) = again {
1956 let access = self.store.mint_access(&key, scope).await?;
1957 let nothing = async |_: &[u8]| Ok(None);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1958 outcome = git_http::forward(
1959 again,
1960 None,
1961 git,
1962 &access,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1963 async |_: &[u8], _: bool| Ok(None),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1964 default_branch.as_deref(),
1965 git_http::PushLimits::default(),
1966 nothing,
1967 )
1968 .await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1969 }
1970 }
Agents as a team: lifecycle, merge queue, billing and a new shell1971 let forwarded =
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1972 match outcome {
Agents as a team: lifecycle, merge queue, billing and a new shell1973 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 API1974 git_http::Push::Refused(response) => {
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1975 after.ended(403, Some("The push was declined by rules.".to_owned()));
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1976 after.spawn(env, ctx);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1977 return Ok(response);
1978 }
1979 git_http::Push::Blocked(response) => {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1980 after.ended(403, Some("The push adds a secret.".to_owned()));
1981 after.spawn(env, ctx);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1982 return Ok(response);
1983 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1984 git_http::Push::Declined(response, reason) => {
1985 after.ended(403, Some(format!("The push was declined: {reason}.")));
1986 after.spawn(env, ctx);
1987 return Ok(response);
1988 }
Agents as a team: lifecycle, merge queue, billing and a new shell1989 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1990 if forwarded.from_store {
1991 let received = forwarded
1992 .response
1993 .headers()
1994 .get("content-length")?
1995 .and_then(|length| length.parse().ok())
1996 .unwrap_or(0);
1997 meters::record(call.meter(), &key, forwarded.sent, received);
1998 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1999 timing.mark("store");
2000 let mut response = forwarded.response;
2001 let status = response.status_code();
2002 if write && !get {
2003 // A push: the store has moved its refs once it has answered in
2004 // full, so the answer is read before the change is recorded, and
2005 // only then goes back. Whoever fetches after it sees the push.
2006 let headers = response.headers().clone();
2007 headers.delete("content-length")?;
2008 let report = response.bytes().await?;
2009 self.refs_moved(&repo.id).await;
2010 timing.mark("refs");
2011 response = Response::from_bytes(report)?.with_headers(headers).with_status(status);
2012 } else if let (Some(kept_key), 200) = (&kept_key, status) {
2013 // A miss: this answer is kept for the next to ask.
2014 let headers = response.headers().clone();
2015 headers.delete("content-length")?;
2016 let body = response.bytes().await?;
2017 if let Some(content_type) = headers.get("content-type")? {
2018 let entry = refs_cache::Entry { content_type, body: body.clone() };
2019 if entry.keepable() {
2020 let shared = self.shared.clone();
2021 let kept_key = kept_key.clone();
2022 ctx.wait_until(async move { refs_cache::keep(shared.as_deref(), &kept_key, &entry).await });
2023 }
2024 }
2025 response = Response::from_bytes(body)?.with_headers(headers).with_status(status);
Merge branch 'worktree-agent-a1b995daa94e4e1b7'2026 } else if let (Some(pack_key), Some(packs), true) = (&pack_key, &self.packs, forwarded.from_store) {
2027 // A fresh clone the bucket did not have: counted, and its pack
2028 // kept as it streams to git, when it is a whole one.
2029 meters::record(pack_cache::MISS, &key, forwarded.sent, 0);
2030 if status == 200 {
2031 let store_key = key.clone();
2032 let measured = Box::new(move |bytes: u64| meters::record_bytes(pack_cache::MISS, &store_key, 0, bytes));
2033 let (teed, filling) = pack_cache::tee(response, packs.clone(), pack_key, measured)?;
2034 response = teed;
2035 if let Some(filling) = filling {
2036 let pack_key = pack_key.clone();
2037 ctx.wait_until(async move {
2038 let filled = filling.await;
2039 if !matches!(filled, pack_cache::Filled::Kept { .. } | pack_cache::Filled::Abandoned) {
2040 worker::console_warn!("pack {} not kept: {filled:?}", pack_key.as_str());
2041 }
2042 });
2043 }
2044 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2045 }
2046 after.ended(status, None);
2047 if status == 200 && (forwarded.pack_bytes > 0 || !forwarded.pushed.is_empty()) {
2048 after.push = Some(PushDone {
2049 repo,
2050 pushed: forwarded.pushed,
2051 pack_bytes: forwarded.pack_bytes,
Merge branch 'worktree-agent-a3abfcce648e87dca'2052 caused_by_job: viewer.as_ref().and_then(g1t_contracts::events::job_run_of).map(str::to_owned),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2053 actor: viewer.map(|user: User| user.id),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2054 unscanned: forwarded.unscanned,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2055 });
2056 }
2057 after.spawn(env, ctx);
2058 Ok(response)
2059 }
2060
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2061 /// The answer for a request a free workspace's limits stop, or a push
2062 /// to a full repository, with its status and reason for the audit log;
2063 /// `None` to go on.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2064 ///
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2065 /// A clone, fetch or push is a git operation, which the git store
2066 /// charges g1t for: counted for billing once the answer has gone back
2067 /// (meters.rs), and a free workspace far past its share is slowed down
2068 /// rather than charged (see git_ops.rs). Whether it is past it is
2069 /// decided from counts this isolate already holds: the database is not
2070 /// asked on the way. A free workspace is never charged for private
2071 /// storage: once its private repositories hold the free amount, pushes
2072 /// to them stop, checked when a push begins so that git shows the
2073 /// 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 200ms2074 async fn git_limits(
2075 &self,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2076 call: git_ops::GitCall,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2077 git: &git_http::GitRequest,
2078 repo: &Repo,
2079 env: &Env,
2080 ) -> Result<Option<(Response, u16, &'static str)>> {
2081 let namespace = git.path.namespace.to_lowercase();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2082 if meters::mapping_now().billable(call.meter()) > 0.0 {
2083 let now = now_ms();
2084 let hour = git_ops::hour_key(&rfc3339(now));
2085 let limits = git_ops::Limits::from_env(env);
2086 if let Some((month, hour_ops)) = git_ops::standing(&namespace, &hour, now)
2087 && git_ops::slow_down(month + 1, hour_ops + 1, limits.free_cap, limits.hourly)
2088 && git_ops::is_free_kept(env.service("BILLING").ok().as_ref(), &namespace).await
2089 {
2090 return Ok(Some((
2091 git_ops::too_many(&namespace, limits.free_cap, limits.hourly)?,
2092 429,
2093 "Too many git operations this hour.",
2094 )));
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2095 }
2096 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2097 if git.service == GitService::ReceivePack && git.endpoint == "info/refs" {
2098 let held = self.held(repo).await;
2099 if held >= self.repo_limit {
2100 let message = format!(
2101 "{}/{} 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",
2102 repo.namespace,
2103 repo.name,
2104 pack_limits::megabytes(held)
2105 );
2106 return Ok(Some((Response::error(message, 403)?, 403, "The repository is full.")));
2107 }
2108 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2109 if git.service == GitService::ReceivePack && git.endpoint == "info/refs" && repo.is_private {
2110 let free = git_ops::free_private_bytes(env);
2111 let held = self.registry.private_bytes(&namespace).await.unwrap_or(0);
2112 if git_ops::storage_full(held, free)
2113 && git_ops::is_free(env.service("BILLING").ok().as_ref(), &namespace).await
2114 {
2115 return Ok(Some((
2116 git_ops::storage_full_response(&namespace, held, free)?,
2117 403,
2118 "Free private storage is full.",
2119 )));
2120 }
2121 }
2122 Ok(None)
2123 }
Rust repos service with shipping; pull requests kept in the model2124
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2125 /// What a repository and its pull requests' working copies hold, as
2126 /// g1t counts it: read for a push's first request, kept a minute for
2127 /// the rest of it.
2128 async fn held(&self, repo: &Repo) -> u64 {
2129 let root = repo.fork_of.clone().unwrap_or_else(|| repo.id.clone());
2130 let now = now_ms();
2131 if let Some(held) = HELD.with(|held| held.borrow().get(&root, now)) {
2132 return held;
2133 }
2134 let held = self.registry.stored_bytes(&root).await.unwrap_or(0).max(0) as u64;
2135 HELD.with(|kept| kept.borrow_mut().put(root, held, now));
2136 held
2137 }
2138
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2139 /// What a push changed, recorded once git has its answer.
2140 async fn record_push(&self, push: PushDone) -> Result<()> {
2141 let PushDone {
2142 repo,
2143 pushed,
2144 pack_bytes,
2145 actor,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2146 unscanned,
Merge branch 'worktree-agent-a3abfcce648e87dca'2147 caused_by_job,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2148 } = push;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2149 // What the push stored, for billing's storage meter. A failure only
2150 // leaves the count short.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2151 if pack_bytes > 0
2152 && 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 put2153 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2154 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 put2155 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2156 if pushed.is_empty() {
2157 return Ok(());
2158 }
Rust repos service with shipping; pull requests kept in the model2159 // Artifacts' own push notifications are per repository, which does
Events service in Rust, with RFC 3339 times and accurate push events2160 // not fit a repo per pull request, so the front end reports pushes
2161 // 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 200ms2162 let stored = self.store.open(&store_key(&repo)).await?;
2163 for pushed in &pushed {
2164 // The store can refuse one ref and accept another, so each
2165 // branch is checked against where it actually is. A tag the
2166 // store cannot read back is taken as pushed.
2167 let moved = match pushed.branch() {
2168 Some(branch) => stored
2169 .log(branch, 1)
2170 .await?
2171 .first()
2172 .is_some_and(|commit| commit.hash == pushed.after),
2173 None => stored.log(&pushed.git_ref, 1).await.map_or(true, |head| {
2174 head.first().is_none_or(|commit| commit.hash == pushed.after)
2175 }),
2176 };
2177 if moved {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2178 self.publish_git_push(
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2179 &repo,
2180 &pushed.git_ref,
2181 pushed.before.as_deref(),
2182 &pushed.after,
2183 actor.clone(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2184 unscanned,
Merge branch 'worktree-agent-a3abfcce648e87dca'2185 caused_by_job.clone(),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2186 )
2187 .await?;
2188 }
2189 }
2190 Ok(())
2191 }
2192}
2193
2194/// A push the store accepted, to be recorded once git has its answer.
2195struct PushDone {
2196 repo: Repo,
2197 pushed: Vec<git_http::Pushed>,
2198 pack_bytes: u64,
2199 actor: Option<String>,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2200 /// Too large to scan for secrets before it was stored.
2201 unscanned: bool,
Merge branch 'worktree-agent-a3abfcce648e87dca'2202 /// The run whose job's token pushed, if one did: its push starts no
2203 /// workflows.
2204 caused_by_job: Option<String>,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2205}
2206
2207/// What a git request leaves for after its answer: its audit entry, with
2208/// how the request ended, and what a push changed.
2209struct AfterGit {
2210 audit: Option<Box<g1t_contracts::audit::NewAuditEntry>>,
2211 status: u16,
2212 message: Option<String>,
2213 push: Option<PushDone>,
2214}
2215
2216impl AfterGit {
2217 fn ended(&mut self, status: u16, message: Option<String>) {
2218 self.status = status;
2219 self.message = message;
2220 }
2221
2222 /// Does the work once the response is on its way. A failure is logged:
2223 /// git has already been told how its request went.
2224 fn spawn(self, env: &Env, ctx: &Context) {
2225 if self.audit.is_none() && self.push.is_none() {
2226 return;
2227 }
2228 let env = env.clone();
2229 ctx.wait_until(async move {
2230 let repos = match service(&env) {
2231 Ok(repos) => repos,
2232 Err(error) => {
2233 worker::console_error!("git request not recorded: {error}");
2234 return;
Events service in Rust, with RFC 3339 times and accurate push events2235 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2236 };
2237 repos.finish_git(self.audit, self.status, self.message).await;
2238 if let Some(push) = self.push
2239 && let Err(error) = repos.record_push(push).await
2240 {
2241 worker::console_error!("push not recorded: {error}");
Rust repos service with shipping; pull requests kept in the model2242 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2243 });
Rust repos service with shipping; pull requests kept in the model2244 }
2245}
2246
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2247fn service(env: &Env) -> Result<Repos<ArtifactsStore>> {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2248 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 look2249 Ok(Repos {
Rust repos service with shipping; pull requests kept in the model2250 registry: Registry { db: env.d1("DB")? },
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2251 store: ArtifactsStore::new(env, shared.clone())?,
2252 shared,
Merge branch 'worktree-agent-aaf03bdceac799c89'2253 packs: pack_cache::Packs::from_env(env).map(Rc::new),
Events service in Rust, with RFC 3339 times and accurate push events2254 events: env.service("EVENTS")?,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API2255 security: env.service("SECURITY").ok(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2256 billing: env.service("BILLING").ok(),
2257 identity: env.service("IDENTITY").ok(),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2258 work: env.service("WORK").ok(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2259 free_private_bytes: git_ops::free_private_bytes(env),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2260 fork_days: forks::retention_days(env),
2261 repo_limit: env
2262 .var("REPO_STORAGE_LIMIT_BYTES")
2263 .ok()
2264 .and_then(|value| value.to_string().parse().ok())
2265 .unwrap_or(pack_limits::DEFAULT_REPO_LIMIT_BYTES),
2266 large_pushes: git_http::LargePushes::from_var(env.var("LARGE_PUSHES").ok().map(|value| value.to_string()).as_deref()),
2267 placement: shards::Placement::from_vars(
2268 env.var("ARTIFACTS_NEW_REPOS").ok().map(|value| value.to_string()).as_deref(),
2269 env.var("ARTIFACTS_EU_NAMESPACE").ok().map(|value| value.to_string()).as_deref(),
2270 ),
Merge branch 'worktree-agent-a2013627e5ea4ab13'2271 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 look2272 })
2273}
2274
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2275/// Writes what this isolate metered once the answer has gone back, every
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2276/// few seconds at most: now, or once it is due, waiting in this request's
2277/// `wait_until` so nothing counted is left for a request that may never
2278/// come (meters.rs).
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2279fn flush_later(env: &Env, ctx: &Context) {
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2280 let Some(wait) = meters::plan_flush() else {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2281 return;
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2282 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2283 if let Ok(db) = env.d1("DB") {
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2284 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 daily2285 }
2286}
2287
Merge branch 'worktree-agent-ac5b181a013e54348'2288/// `/backups/<job id>/parts/<number>`: the job and the part's number.
2289fn backup_part_path(path: &str) -> Option<(String, u16)> {
2290 let rest = path.strip_prefix("/backups/")?;
2291 let (job, number) = rest.split_once("/parts/")?;
2292 let number = number.parse::<u16>().ok()?;
2293 (!job.is_empty() && !job.contains('/')).then(|| (job.to_owned(), number))
2294}
2295
2296fn backups_off<T>() -> Outcome<T> {
2297 Outcome::fail(FailureCode::Conflict, "Backups are off on this installation: it has no storage for them.")
2298}
2299
2300/// One part of a backup's bundle, with the job's token in its header.
2301async fn backup_part(request: &mut Request, env: &Env, repos: &Repos<ArtifactsStore>, job_id: String, number: u16) -> Result<Response> {
2302 let Some(blobs) = backups::storage(env) else {
2303 return reply(&backups_off::<()>());
2304 };
2305 let token = request.headers().get(g1t_contracts::backups::TOKEN_HEADER)?.unwrap_or_default();
2306 let bytes = request.bytes().await?;
2307 let job = g1t_contracts::backups::BackupJobArgs { job_id, token };
2308 reply(&backups::part(&repos.registry.db, &blobs, &job, number, bytes).await?)
2309}
2310
2311#[cfg(test)]
2312mod backup_path_tests {
2313 use super::backup_part_path;
2314
2315 #[test]
2316 fn a_part_is_named_by_its_job_and_number() {
2317 assert_eq!(backup_part_path("/backups/bkp_1/parts/3"), Some(("bkp_1".to_owned(), 3)));
2318 assert_eq!(backup_part_path("/backups/bkp_1/parts/x"), None);
2319 assert_eq!(backup_part_path("/backups//parts/1"), None);
2320 assert_eq!(backup_part_path("/acme/rocket.git/info/refs"), None);
2321 }
2322}
2323
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2324/// Read methods whose answer is an `Outcome`: when the git store is busy,
2325/// the site is told so in words instead of failing the page.
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 252326const OUTCOME_READS: [&str; 7] = ["tree", "blob", "log", "branches", "blame", "compare", "branch_drift"];
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2327
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2328#[event(fetch)]
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2329async fn fetch(mut request: Request, env: Env, ctx: Context) -> Result<Response> {
Fast pages, required checks on the branch, self-hosted runners, honest incidents2330 let mut repos = service(&env)?;
Merge branch 'worktree-agent-ac5b181a013e54348'2331 // A part of a backup's bundle, as the API passes it on from the
2332 // sandbox: bytes, not JSON (backups.rs).
2333 if request.method() == Method::Put
2334 && let Some((job_id, number)) = backup_part_path(&request.path())
2335 {
2336 let answered = backup_part(&mut request, &env, &repos, job_id, number).await;
2337 flush_later(&env, &ctx);
2338 return answered;
2339 }
Rust repos service with shipping; pull requests kept in the model2340 let Some(method) = rpc_method(&request) else {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2341 let answered = repos.git_http(request, &env, &ctx).await;
2342 flush_later(&env, &ctx);
2343 return answered;
Rust repos service with shipping; pull requests kept in the model2344 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents2345 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
2346 // Git over HTTPS above always reads the primary.
2347 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
2348 repos.registry.db = db;
Rust repos service with shipping; pull requests kept in the model2349 let body: serde_json::Value = request.json().await?;
2350
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2351 let answered = async { match method.as_str() {
Rust repos service with shipping; pull requests kept in the model2352 "get" => reply(&repos.get(args(body)?).await?),
2353 "get_by_id" => reply(&repos.get_by_id(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2354 "readable" => {
2355 let a: ReadableArgs = args(body)?;
2356 reply(&repos.registry.readable(&a.ids, &a.viewer).await?)
2357 }
2358 "public_namespaces" => {
2359 let a: PublicNamespacesArgs = args(body)?;
2360 reply(&repos.registry.public_namespaces(&a.owner_id).await?)
2361 }
Automations: rules in .g1t/automations that act when something happens2362 "path_by_id" => {
2363 let a: PathByIdArgs = args(body)?;
2364 reply(
2365 &repos
2366 .registry
2367 .by_id(&a.id)
2368 .await?
2369 .filter(|repo| repo.fork_of.is_none())
2370 .map(|repo| RepoPath {
2371 namespace: repo.namespace,
2372 name: repo.name,
2373 }),
2374 )
2375 }
Rust repos service with shipping; pull requests kept in the model2376 "list" => {
2377 let a: ListArgs = args(body)?;
2378 reply(
2379 &repos
2380 .registry
Workspaces own repositories2381 .list(
2382 &a.viewer,
2383 a.query.as_deref(),
2384 a.namespace.as_deref(),
2385 a.member_only,
2386 )
Rust repos service with shipping; pull requests kept in the model2387 .await?,
2388 )
2389 }
2390 "create" => reply(&repos.create(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2391 // Services only: a GitHub mirror catching up, or pushing out.
2392 "mirror" => reply(&repos.mirror(args(body)?).await?),
2393 "transfer" => reply(&repos.transfer(args(body)?).await?),
2394 // A repository's lifecycle: see lifecycle.rs.
2395 "delete" => reply(&repos.delete(args(body)?).await?),
2396 "deleted" => reply(&repos.deleted(args(body)?).await?),
2397 "restore" => reply(&repos.restore(args(body)?).await?),
2398 "purge" => reply(&repos.purge(args(body)?).await?),
2399 "purge_due" => reply(&repos.purge_due(args(body)?).await?),
2400 "rename" => reply(&repos.rename(args(body)?).await?),
2401 "archive" => reply(&repos.archive(args(body)?).await?),
2402 "set_visibility" => reply(&repos.set_visibility(args(body)?).await?),
2403 "set_default_branch" => reply(&repos.set_default_branch(args(body)?).await?),
2404 "rename_branch" => reply(&repos.rename_branch(args(body)?).await?),
2405 "resolve_branch" => reply(&repos.resolve_branch(args(body)?).await?),
2406 "status_by_id" => reply(&repos.status_by_id(args(body)?).await?),
2407 "resolve_path" => {
2408 let a: ResolvePathArgs = args(body)?;
2409 reply(&repos.registry.resolve_moved(&a.path).await?)
2410 }
2411 "namespace_count" => {
2412 let a: NamespaceCountArgs = args(body)?;
2413 reply(&repos.registry.count_in(&a.namespace).await?)
2414 }
Agents as a team: lifecycle, merge queue, billing and a new shell2415 "update" => reply(&repos.update(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model2416 "tree" => reply(&repos.tree(args(body)?).await?),
2417 "blob" => reply(&repos.blob(args(body)?).await?),
2418 "log" => reply(&repos.log(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell2419 "blame" => reply(&repos.blame(args(body)?).await?),
Issues and pull requests replace intents and attempts2420 "fork_for_pull" => reply(&repos.fork_for_pull(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model2421 "git_access" => reply(&repos.git_access(args(body)?).await?),
Pull requests from branches2422 "branches" => reply(&repos.branches(args(body)?).await?),
Branches and Tags pages, each file's last commit, and the branch menu on files2423 "last_commits" => reply(&repos.last_commits(args(body)?).await?),
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 252424 "branch_drift" => reply(&repos.branch_drift(args(body)?).await?),
Branches and Tags pages, each file's last commit, and the branch menu on files2425 "tags" => reply(&repos.tags(args(body)?).await?),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb972426 // The About: what the Files page shows beside the files (about.rs).
2427 // What is kept behind the head is worked out again after the answer.
2428 "about" => {
2429 let (answer, refresh) = repos.about(args(body)?).await?;
2430 about::refresh_later(&env, &ctx, refresh);
2431 reply(&answer)
2432 }
2433 "languages" => {
2434 let (answer, refresh) = repos.languages(args(body)?).await?;
2435 about::refresh_later(&env, &ctx, refresh);
2436 reply(&answer)
2437 }
2438 "contributors" => {
2439 let (answer, refresh) = repos.contributors(args(body)?).await?;
2440 about::refresh_later(&env, &ctx, refresh);
2441 reply(&answer)
2442 }
2443 "license" => {
2444 let (answer, refresh) = repos.license(args(body)?).await?;
2445 about::refresh_later(&env, &ctx, refresh);
2446 reply(&answer)
2447 }
2448 "stars" => reply(&repos.stars(args(body)?).await?),
2449 "star" => reply(&repos.star(args(body)?).await?),
2450 "stargazers" => reply(&repos.stargazers(args(body)?).await?),
2451 "starred" => reply(&repos.starred(args(body)?).await?),
2452 "releases" => reply(&repos.releases(args(body)?).await?),
2453 "release" => reply(&repos.release(args(body)?).await?),
2454 "create_release" => reply(&repos.create_release(args(body)?).await?),
2455 "update_release" => reply(&repos.update_release(args(body)?).await?),
2456 "delete_release" => reply(&repos.delete_release(args(body)?).await?),
Pull requests from branches2457 "head" => reply(&repos.head(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell2458 "behind" => reply(&repos.behind(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2459 "divergence" => reply(&repos.divergence(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model2460 "land" => reply(&repos.land(args(body)?).await?),
Catching up with main takes seconds when the two sides touched different files2461 "update_pull_branch" => reply(&repos.update_pull_branch(args(body)?).await?),
Merge queue: tested states are deleted once their entry leaves2462 "delete_branch" => reply(&repos.delete_branch(args(body)?).await?),
Fast pages, required checks on the branch, self-hosted runners, honest incidents2463 "commit_file" => reply(&repos.commit_file(args(body)?).await?),
Diffs on attempts; hosted agent presented as the g1t agent2464 "compare" => reply(&repos.compare(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2465 // Services only: a pull request's commits, as rules look at them (rules.rs).
2466 "inspect_commits" => reply(&repos.inspect_commits(args(body)?).await?),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API2467 "scan_history" => reply(&repos.scan_history(args(body)?).await?),
2468 "find_lockfiles" => reply(&repos.find_lockfiles(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2469 "match_pattern" => reply(&repos.match_pattern(args(body)?).await?),
2470 "check_secret" => reply(&repos.check_secret(args(body)?).await?),
Search across all of g1t, Explore, and a command palette2471 "list_files" => reply(&repos.list_files(args(body)?).await?),
2472 "changed_files" => reply(&repos.changed_files(args(body)?).await?),
2473 "read_blobs" => reply(&repos.read_blobs(args(body)?).await?),
Composer from the workspace's own repositories, and go get from g1t.sh2474 // Services only: what the Composer registry builds packages from.
2475 "refs" => reply(&repos.refs_of(args(body)?).await?),
2476 "raw_file" => reply(&repos.raw_file(args(body)?).await?),
2477 "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 put2478 "visibility" => {
2479 let a: g1t_contracts::repos::VisibilityArgs = args(body)?;
2480 reply(&repos.registry.visibility(&a.paths).await?)
2481 }
2482 "storage" => reply(&repos.registry.storage().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2483 "git_operations" => {
2484 let a: GitOperationsArgs = args(body)?;
2485 reply(&git_ops::totals(&repos.registry.db, &a.month, a.since.as_deref(), a.namespace.as_deref().map(str::to_lowercase).as_deref()).await?)
2486 }
Merge main (membership, two-factor, GitHub repo roles) into tokens2487 // Identity, once: who created each repository (members.rs there).
2488 "repo_creators" => {
2489 let a: AllIdsArgs = args(body)?;
2490 let limit = a.limit.clamp(1, 500);
2491 let repos = repos.registry.creators_after(a.after.as_deref(), limit).await?;
2492 let next = (repos.len() == limit as usize).then(|| repos.last().map(|repo| repo.id.clone())).flatten();
2493 reply(&CreatorPage { repos, next })
2494 }
Search across all of g1t, Explore, and a command palette2495 "all_ids" => {
2496 let a: AllIdsArgs = args(body)?;
2497 let limit = a.limit.clamp(1, 500);
2498 let ids = repos.registry.ids_after(a.after.as_deref(), limit).await?;
2499 let next = (ids.len() == limit as usize).then(|| ids.last().cloned()).flatten();
2500 reply(&IdPage { ids, next })
2501 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2502 // The raw meters of the git store, for reconciling with Cloudflare
2503 // (meters.rs, scripts/ops/artifacts-usage.mjs).
2504 "artifacts_usage" => {
2505 let a: meters::UsageArgs = args(body)?;
2506 reply(&meters::usage(&repos.registry.db, &a).await?)
2507 }
2508 "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)2509 // Billing: the workspace each pull request's working copy is counted
2510 // for, so Cloudflare's own count of `pulls--<id>` shares out too.
2511 "pull_owners" => {
2512 #[derive(serde::Deserialize)]
2513 struct PullOwnersArgs {
2514 pulls: Vec<String>,
2515 }
2516 let a: PullOwnersArgs = args(body)?;
2517 let pulls: Vec<String> = a.pulls.into_iter().take(500).collect();
2518 reply(&serde_json::json!({ "owners": meters::pull_owners(&repos.registry.db, &pulls).await? }))
2519 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2520 // Services only: which meters are operations, changed without a deploy.
2521 "set_operation_mapping" => {
2522 let row: meters::MappingRow = args(body)?;
2523 meters::set_mapping(&repos.registry.db, &row, &rfc3339(now_ms())).await?;
2524 reply(&meters::read_mapping(&repos.registry.db).await?)
2525 }
Merge branch 'worktree-agent-ac5b181a013e54348'2526 // Backups (backups.rs): the runner's sweep claims queued ones, and
2527 // each sandbox, through the API, asks for its job and says how it went.
2528 "claim_backups" => {
2529 let a: g1t_contracts::backups::ClaimBackupsArgs = args(body)?;
2530 let blobs = backups::storage(&env);
2531 reply(&backups::claim(&repos.registry.db, blobs.as_ref(), &a, now_ms()).await?)
2532 }
2533 "backup_spec" => match backups::storage(&env) {
2534 Some(blobs) => {
2535 let a: g1t_contracts::backups::BackupJobArgs = args(body)?;
2536 let every = backups::Settings::from_env(&env).full_every;
2537 reply(&backups::spec(&repos.registry, &blobs, &repos.store, &a, every, now_ms()).await?)
2538 }
2539 None => reply(&backups_off::<bool>()),
2540 },
2541 "backup_complete" => match backups::storage(&env) {
2542 Some(blobs) => reply(&backups::complete(&repos.registry, &blobs, &args(body)?, now_ms()).await?),
2543 None => reply(&backups_off::<bool>()),
2544 },
2545 "backup_fail" => match backups::storage(&env) {
2546 Some(blobs) => reply(&backups::fail(&repos.registry.db, &blobs, &args(body)?).await?),
2547 None => reply(&backups_off::<bool>()),
2548 },
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2549 // How the git store has been answering, for the status page.
2550 "store_health" => {
2551 let a: meters::HealthArgs = args(body)?;
2552 reply(&meters::health(&repos.registry.db, &a).await?)
2553 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'2554 // Where repositories may be kept, for a workspace's settings.
2555 "storage_options" => reply(&repos.storage_options()),
2556 // Services and operators only: how each namespace stands, and
2557 // moving a repository between them (namespaces.rs, moves.rs).
2558 "namespaces" => reply(&repos.standings().await?),
2559 "move_repository" => reply(&repos.move_repository(args(body)?).await?),
2560 "repository_moves" => {
2561 let a: moves::ListMovesArgs = args(body)?;
2562 reply(&repos.registry.moves(a.limit.unwrap_or(50)).await?)
2563 }
Rust repos service with shipping; pull requests kept in the model2564 _ => Response::error("Unknown method", 404),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2565 } }
2566 .await;
2567 // The git store is busy: said in words, with when to try again.
2568 let answered = match answered {
2569 Err(error) => match resilience::busy(&error.to_string()) {
2570 Some(busy) if OUTCOME_READS.contains(&method.as_str()) => {
2571 reply(&Outcome::<()>::fail(FailureCode::Conflict, busy.message().trim()))
2572 }
2573 Some(busy) => {
2574 let response = Response::error(busy.message(), 503)?;
2575 response.headers().set("retry-after", &busy.retry_after.to_string())?;
2576 Ok(response)
2577 }
2578 None => Err(error),
2579 },
2580 answered => answered,
Fast pages, required checks on the branch, self-hosted runners, honest incidents2581 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2582 flush_later(&env, &ctx);
Fast pages, required checks on the branch, self-hosted runners, honest incidents2583 served.finish(answered)
Rust repos service with shipping; pull requests kept in the model2584}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2585
Merge branch 'worktree-agent-ac5b181a013e54348'2586/// The nightly cron in wrangler.jsonc: tonight's backups are queued.
2587const BACKUP_CRON: &str = "53 2 * * *";
2588
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2589/// The hourly sweep: deleted repositories whose time to be restored has
Merge branch 'worktree-agent-ac5b181a013e54348'2590/// passed are purged. See lifecycle.rs. And, at [`BACKUP_CRON`], the
2591/// 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 look2592#[event(scheduled)]
Merge branch 'worktree-agent-ac5b181a013e54348'2593async fn scheduled(event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2594 let repos = match service(&env) {
2595 Ok(repos) => repos,
2596 Err(error) => {
2597 worker::console_error!("repos: the sweep could not start: {error}");
2598 return;
2599 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2600 };
Merge branch 'worktree-agent-ac5b181a013e54348'2601 if event.cron() == BACKUP_CRON {
2602 let Some(blobs) = backups::storage(&env) else { return };
2603 match backups::nightly(&repos.registry.db, &blobs, backups::Settings::from_env(&env), now_ms()).await {
2604 Ok(night) => worker::console_log!("repos: queued {} backups, removed {} of purged repositories", night.queued, night.pruned),
2605 Err(error) => worker::console_error!("repos: backups could not be queued: {error}"),
2606 }
2607 return;
2608 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2609 match repos.purge_due(PurgeDueArgs::default()).await {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2610 Ok(0) => {}
2611 Ok(count) => worker::console_log!("repos: purged {count} deleted repositories"),
2612 Err(error) => worker::console_error!("repos: the purge sweep failed: {error}"),
2613 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2614 // Pull requests' working copies whose time has come (forks.rs).
2615 match repos.retire_due().await {
2616 Ok(0) => {}
2617 Ok(count) => worker::console_log!("repos: removed {count} pull request working copies"),
2618 Err(error) => worker::console_error!("repos: the working copy sweep failed: {error}"),
2619 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'2620 // Repositories moving between namespaces, and old copies (moves.rs).
2621 match repos.run_moves().await {
2622 Ok(0) => {}
2623 Ok(count) => worker::console_log!("repos: moved {count} repositories between namespaces"),
2624 Err(error) => worker::console_error!("repos: the move sweep failed: {error}"),
2625 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2626 meters::flush(&repos.registry.db).await;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2627}
2628
2629/// Events from the bus. A workspace's rename: its repositories move to the
2630/// workspace's current slug, asked of identity by id, so a repeated or late
2631/// 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 member2632/// were. A workspace's deletion: its repositories are deleted with it,
2633/// restored with it, or purged with it.
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2634#[event(queue)]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2635async fn queue(batch: MessageBatch<Event>, env: Env, ctx: Context) -> Result<()> {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2636 let registry = Registry { db: env.d1("DB")? };
2637 let identity = env.service("IDENTITY")?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2638 let handled = handle_events(&batch, &env, &registry, &identity).await;
2639 flush_later(&env, &ctx);
2640 handled
2641}
2642
2643async fn handle_events(batch: &MessageBatch<Event>, env: &Env, registry: &Registry, identity: &Fetcher) -> Result<()> {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2644 for message in batch.messages()? {
2645 let event = message.body();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2646 // A pull request merged, closed or reopened: its working copy is
2647 // kept or let go (forks.rs).
2648 if let Some(change) = forks::pull_change(&event.kind) {
2649 let Some(pull_id) = forks::pull_id_of(&event.data) else {
2650 worker::console_error!("{} {} names no pull request", event.kind, event.id);
2651 continue;
2652 };
2653 let repos = service(env)?;
2654 match change {
2655 forks::PullChange::Settled => repos.pull_settled(&pull_id).await?,
2656 forks::PullChange::Reopened => repos.pull_reopened(&pull_id).await?,
2657 }
2658 continue;
2659 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member2660 // A workspace deleted, restored or purged: its repositories go with
2661 // it, come back with it, or are purged with it (lifecycle.rs).
2662 if event.kind == "workspace.deleting" {
2663 match serde_json::from_value::<WorkspaceDeleting>(event.data.clone()) {
2664 Ok(deleting) => service(env)?.delete_with_workspace(&deleting, &protected_workspaces(env)).await?,
2665 Err(_) => worker::console_error!("workspace.deleting {} could not be read", event.id),
2666 }
2667 continue;
2668 }
2669 if event.kind == "workspace.restored" {
2670 match serde_json::from_value::<WorkspaceRestored>(event.data.clone()) {
2671 Ok(restored) => service(env)?.restore_with_workspace(&restored).await?,
2672 Err(_) => worker::console_error!("workspace.restored {} could not be read", event.id),
2673 }
2674 continue;
2675 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2676 if event.kind == "workspace.deleted" {
2677 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 member2678 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 look2679 Err(_) => worker::console_error!("workspace.deleted {} could not be read", event.id),
2680 }
2681 continue;
2682 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2683 if event.kind != "workspace.renamed" {
2684 continue;
2685 }
2686 let Ok(renamed) = serde_json::from_value::<WorkspaceRenamed>(event.data.clone()) else {
2687 worker::console_error!("workspace.renamed {} could not be read", event.id);
2688 continue;
2689 };
2690 let names: HashMap<String, String> = g1t_kit::call(
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2691 identity,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2692 "usernames",
2693 &g1t_contracts::identity::UsernamesArgs {
2694 ids: vec![renamed.workspace_id.clone()],
2695 },
2696 )
2697 .await?;
2698 let current = names
2699 .get(&renamed.workspace_id)
2700 .cloned()
2701 .unwrap_or_else(|| renamed.to.clone());
2702 let left = registry
2703 .rename_namespace(&renamed.stale_slugs(&current), &current)
2704 .await?;
2705 if left > 0 {
2706 worker::console_error!(
2707 "{left} repositories stayed under {} or {}: {current} already has repositories of the same names",
2708 renamed.from,
2709 renamed.to
2710 );
2711 }
2712 }
2713 Ok(())
2714}
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2715
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member2716/// The workspaces whose repositories never go with a deletion, whatever is
2717/// published: `PROTECTED_WORKSPACES` if set here, and Flagon's always.
2718fn protected_workspaces(env: &Env) -> Vec<String> {
2719 let configured = env.var("PROTECTED_WORKSPACES").ok().map(|v| v.to_string());
2720 g1t_contracts::identity::protected_names(configured.as_deref())
2721}
2722
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca2723/// What a token's own limits say about git on the repository `repo`
2724/// (`owner/name`), before anyone's role is asked: why it is refused, or
2725/// `None`. `source` is the repository a pull request's working copy at
2726/// `repo` belongs to, which a workflow job's token reaches too. `public`
2727/// is whether anyone may read it, and `exists` whether there is one.
2728///
2729/// A job's token and a deploy key reach their own repository only. Its
2730/// scopes decide the rest: `code:read` to read a private repository,
2731/// `code:write` to push, which a read-only deploy key never has. A deploy
2732/// key never makes a repository by pushing to an empty address.
2733pub(crate) fn git_token_refusal(
2734 access: &g1t_contracts::scopes::TokenAccess,
2735 repo: &str,
2736 source: Option<&str>,
2737 write: bool,
2738 public: bool,
2739 exists: bool,
2740) -> Option<String> {
2741 if let Some(refused) = g1t_contracts::scopes::decide_repo(access, repo)
2742 && !source.is_some_and(|source| access.reaches(source))
2743 {
2744 return Some(refused.reason.unwrap_or_default());
2745 }
2746 let decision = g1t_contracts::scopes::decide_git(access, write, public);
2747 if !decision.allowed {
2748 return Some(decision.reason.unwrap_or_default());
2749 }
2750 if access.deploy_key.is_some() && !exists {
2751 return Some(format!("This deploy key is for {repo}, which is not there any more."));
2752 }
2753 None
2754}
2755
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2756/// The repository a push to a path that does not exist yet creates: private,
2757/// so nothing pushed by mistake is published. An owner makes it public on
2758/// purpose (`POST /repos/{owner}/{repo}/visibility`).
2759fn push_to_create(owner: &User, path: &RepoPath) -> CreateArgs {
2760 CreateArgs {
2761 owner: owner.clone(),
2762 namespace: path.namespace.clone(),
2763 name: path.name.clone(),
2764 description: None,
2765 is_private: true,
2766 import_url: None,
2767 import_token: None,
2768 }
2769}
2770
2771#[cfg(test)]
2772mod push_to_create_tests {
2773 use super::*;
2774
2775 #[test]
2776 fn a_pushed_repository_starts_private() {
2777 let owner: User = serde_json::from_value(serde_json::json!({ "id": "usr_1", "username": "ada" })).unwrap();
2778 let args = push_to_create(&owner, &RepoPath { namespace: "acme".into(), name: "site".into() });
2779 assert!(args.is_private);
2780 assert_eq!((args.namespace.as_str(), args.name.as_str()), ("acme", "site"));
2781 }
2782}
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca2783
2784#[cfg(test)]
2785mod deploy_key_git_tests {
2786 use super::*;
2787 use g1t_contracts::deploy_keys;
2788
2789 fn key(read_only: bool) -> User {
2790 deploy_keys::principal("wsp_acme", "acme", deploy_keys::access("dk_1", "CI", "acme/rocket", read_only))
2791 }
2792
2793 fn rocket(private: bool) -> Repo {
2794 serde_json::from_value(serde_json::json!({
2795 "id": "rep_rocket",
2796 "namespace": "acme",
2797 "name": "rocket",
2798 "description": null,
2799 "isPrivate": private,
2800 "ownerId": "usr_owner",
2801 "defaultBranch": "main",
2802 "forkOf": null,
2803 "protected": false,
2804 "createdAt": "",
2805 }))
2806 .unwrap()
2807 }
2808
2809 fn refusal(user: &User, repo: &str, write: bool, exists: bool) -> Option<String> {
2810 git_token_refusal(user.token.as_deref().unwrap(), repo, None, write, false, exists)
2811 }
2812
2813 #[test]
2814 fn a_read_only_deploy_key_clones_its_repository_and_never_pushes() {
2815 let user = key(true);
2816 assert_eq!(refusal(&user, "acme/rocket", false, true), None);
2817 assert!(refusal(&user, "acme/rocket", true, true).unwrap().contains("read-only"));
2818 // Its role is a workspace token's: it reads a private repository.
2819 assert!(registry::can_read(&rocket(true), &Some(user)));
2820 }
2821
2822 #[test]
2823 fn a_deploy_key_with_write_access_pushes_to_its_repository() {
2824 let user = key(false);
2825 assert_eq!(refusal(&user, "acme/rocket", true, true), None);
2826 assert!(registry::can_write(&rocket(true), &Some(user)));
2827 }
2828
2829 #[test]
2830 fn a_deploy_key_reaches_no_other_repository() {
2831 let user = key(false);
2832 for other in ["acme/booster", "other/rocket"] {
2833 for write in [false, true] {
2834 let why = refusal(&user, other, write, true).expect(other);
2835 assert!(why.contains("deploy key is for acme/rocket"), "{why}");
2836 }
2837 }
2838 // Not even a pull request's working copy of another repository.
2839 let token = user.token.as_deref().unwrap();
2840 assert!(git_token_refusal(token, "pulls/pr_1", Some("acme/booster"), false, false, true).is_some());
2841 }
2842
2843 #[test]
2844 fn a_deploy_key_never_creates_a_repository() {
2845 let why = refusal(&key(false), "acme/rocket", true, false).unwrap();
2846 assert!(why.contains("not there"), "{why}");
2847 }
2848}

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