Skip to content
2,907 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;
Merge last commits over the whole history: progress kept per ref and path, a push reads only its own commits80/// How long a `last_commits` walk asked without a budget runs before it
81/// stops and keeps its progress for the next call. The site asks from a
82/// waitUntil, which may run 30 s past its response.
83const LAST_COMMITS_WALK_MS: u64 = 20_000;
Branches and Tags pages, each file's last commit, and the branch menu on files84
85/// One path segment, percent-encoded for a cache key.
86fn urlencoding_segment(segment: &str) -> String {
87 segment
88 .bytes()
89 .map(|b| if b.is_ascii_alphanumeric() || b"-._~".contains(&b) { (b as char).to_string() } else { format!("%{b:02X}") })
90 .collect()
91}
Agents as a team: lifecycle, merge queue, billing and a new shell92const MAX_DESCRIPTION_CHARS: usize = 200;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look93pub(crate) const SOURCE: &str = "repos";
94pub(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 model95
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look96pub(crate) fn not_found<T>() -> Outcome<T> {
Rust repos service with shipping; pull requests kept in the model97 Outcome::fail(FailureCode::NotFound, "Repository not found.")
98}
99
100/// Decoded text, or `None` when the file is too large or looks binary.
Agents as a team: lifecycle, merge queue, billing and a new shell101/// Whether a ref is a full commit hash rather than a branch name.
102fn is_commit_hash(git_ref: &str) -> bool {
103 git_ref.len() == 40 && git_ref.bytes().all(|b| b.is_ascii_hexdigit())
104}
105
Rust repos service with shipping; pull requests kept in the model106fn text_of(bytes: Vec<u8>) -> Option<String> {
107 if bytes.len() > MAX_TEXT_BYTES || bytes.contains(&0) {
108 return None;
109 }
110 Some(String::from_utf8_lossy(&bytes).into_owned())
111}
112
113fn is_readme(name: &str) -> bool {
114 matches!(
115 name.to_lowercase().as_str(),
116 "readme" | "readme.md" | "readme.markdown" | "readme.txt"
117 )
118}
119
120/// Whether `ancestor` is reachable from the newest commit in `history`.
121///
122/// `history` is the first-parent chain, which is all the store lists; a fork
123/// that merged the target branch in has the target's head on a second
124/// parent, so the walk follows every parent.
125async fn descends_from<R: GitRepo>(repo: &R, history: &[Commit], ancestor: &str) -> Result<bool> {
126 let known: HashMap<&str, &[String]> = history
127 .iter()
128 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
129 .collect();
130 let mut seen = HashSet::new();
131 let mut queue: Vec<String> = history
132 .first()
133 .map(|c| c.hash.clone())
134 .into_iter()
135 .collect();
136 while let Some(hash) = queue.pop() {
137 if hash == ancestor {
138 return Ok(true);
139 }
140 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
141 continue;
142 }
143 match known.get(hash.as_str()) {
144 Some(parents) => queue.extend(parents.iter().cloned()),
145 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
146 }
147 }
148 Ok(false)
149}
150
Diffs on attempts; hosted agent presented as the g1t agent151/// The commit closest to the newest in `history` that is also in `shared`:
152/// where a fork and the repository it came from last agreed.
153async fn nearest_ancestor_in<R: GitRepo>(
154 repo: &R,
155 history: &[Commit],
156 shared: &HashSet<String>,
157) -> Result<Option<String>> {
158 let known: HashMap<&str, &[String]> = history
159 .iter()
160 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
161 .collect();
162 let mut seen = HashSet::new();
163 let mut queue: VecDeque<String> = history
164 .first()
165 .map(|c| c.hash.clone())
166 .into_iter()
167 .collect();
168 while let Some(hash) = queue.pop_front() {
169 if shared.contains(&hash) {
170 return Ok(Some(hash));
171 }
172 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
173 continue;
174 }
175 match known.get(hash.as_str()) {
176 Some(parents) => queue.extend(parents.iter().cloned()),
177 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
178 }
179 }
180 Ok(None)
181}
182
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily183thread_local! {
184 /// Targets' sides of mergeability, by head (coalesce.rs).
185 static TARGETS: std::cell::RefCell<coalesce::Memo<coalesce::TargetKey, Rc<coalesce::TargetSide>>> =
186 std::cell::RefCell::new(coalesce::Memo::new(coalesce::TARGET_TTL_MS, 32));
187 /// What targets changed between two trees.
188 static THEIRS: std::cell::RefCell<coalesce::Memo<coalesce::TheirsKey, (Vec<String>, bool)>> =
189 std::cell::RefCell::new(coalesce::Memo::new(coalesce::THEIRS_TTL_MS, 256));
190 /// What repositories hold, as read for a push's first request, for the
191 /// same push's second: a push's POST does not wait on the database.
192 static HELD: std::cell::RefCell<coalesce::Memo<String, u64>> =
193 std::cell::RefCell::new(coalesce::Memo::new(60_000, 512));
194}
195
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look196pub(crate) struct Repos<S: GitStore> {
Rust repos service with shipping; pull requests kept in the model197 registry: Registry,
198 store: S,
Events service in Rust, with RFC 3339 times and accurate push events199 events: Fetcher,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API200 /// Asked during a push which secrets have been allowed.
201 security: Option<Fetcher>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look202 /// Asked whether a workspace is on a plan, for its private storage.
203 billing: Option<Fetcher>,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge204 /// Says which rulesets hold for a change to a branch or tag, and keeps
205 /// how they judged it (rules.rs). `None` where it is not deployed: the
206 /// old protection flag then holds on push.
207 work: Option<Fetcher>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look208 /// Told when a repository moves, for the tokens of agents at work on it.
209 identity: Option<Fetcher>,
210 /// What a free workspace's private repositories may hold.
211 free_private_bytes: i64,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily212 /// Days a pull request's working copy is kept after it settles (forks.rs).
213 pub(crate) fork_days: u64,
214 /// The most a repository may hold (pack_limits.rs), and what happens
215 /// to a push too large to scan.
216 repo_limit: u64,
217 large_pushes: git_http::LargePushes,
Merge branch 'worktree-agent-a2013627e5ea4ab13'218 /// Which git store namespace new repositories go in (shards.rs), and
219 /// the most each should hold (`ARTIFACTS_NAMESPACE_LIMITS`).
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily220 placement: shards::Placement,
Merge branch 'worktree-agent-a2013627e5ea4ab13'221 limits: HashMap<String, u64>,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms222 /// What isolates share: answers that list refs (refs_cache.rs).
223 shared: Option<Rc<shared::Shared>>,
Merge branch 'worktree-agent-a1b995daa94e4e1b7'224 /// Packs for fresh clones (pack_cache.rs); `None` without the bucket.
Merge branch 'worktree-agent-aaf03bdceac799c89'225 packs: Option<Rc<pack_cache::Packs>>,
Rust repos service with shipping; pull requests kept in the model226}
227
228impl<S: GitStore> Repos<S> {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms229 /// Records that the refs of the repository with this id changed, once
230 /// they have, so that the answers kept that list them go stale (see
231 /// refs_cache.rs). Everything that changes a repository's refs calls
232 /// this after it (`every_ref_writer_records_the_change` checks). A
233 /// failure is logged: the change itself happened, and what was kept
234 /// expires within `refs_cache::TTL_SECONDS` regardless.
235 pub(crate) async fn refs_moved(&self, repo_id: &str) {
236 if let Err(error) = self.registry.refs_moved(repo_id).await {
237 worker::console_error!("refs of {repo_id} changed but not recorded: {error}");
238 }
239 }
240
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look241 pub(crate) async fn publish<T: Serialize>(&self, event: NewEvent<T>) -> Result<()> {
Events service in Rust, with RFC 3339 times and accurate push events242 g1t_kit::call(
243 &self.events,
244 "publish",
245 &Publish {
246 events: vec![event],
247 },
248 )
249 .await
Rust repos service with shipping; pull requests kept in the model250 }
251
Members can read a private repository's pull request forks252 /// Whether the viewer may read `repo`. A pull request's fork of a
253 /// private repository can be read by everyone who can read that
254 /// repository, so its members can review and check out the change, as
255 /// well as by whoever opened the pull request.
256 async fn may_read(&self, repo: &Repo, viewer: &Viewer) -> Result<bool> {
257 if can_read(repo, viewer) {
258 return Ok(true);
259 }
260 let Some(source_id) = &repo.fork_of else {
261 return Ok(false);
262 };
Rust repos service with shipping; pull requests kept in the model263 Ok(self
264 .registry
Members can read a private repository's pull request forks265 .by_id(source_id)
Rust repos service with shipping; pull requests kept in the model266 .await?
Members can read a private repository's pull request forks267 .is_some_and(|source| can_read(&source, viewer)))
Rust repos service with shipping; pull requests kept in the model268 }
269
Members can read a private repository's pull request forks270 /// `repo`, if there is one and the viewer may read it.
271 async fn visible(&self, repo: Option<Repo>, viewer: &Viewer) -> Result<Option<Repo>> {
272 Ok(match repo {
273 Some(repo) if self.may_read(&repo, viewer).await? => Some(repo),
274 _ => None,
275 })
276 }
277
278 /// 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 look279 pub(crate) async fn readable(&self, path: &RepoPath, viewer: &Viewer) -> Result<Option<Repo>> {
Members can read a private repository's pull request forks280 self.visible(self.registry.by_path(path).await?, viewer)
281 .await
282 }
283
Rust repos service with shipping; pull requests kept in the model284 async fn get(&self, a: GetArgs) -> Result<Outcome<Repo>> {
285 Ok(self
286 .readable(&a.path, &a.viewer)
287 .await?
288 .map_or_else(not_found, Outcome::Ok))
289 }
290
291 async fn get_by_id(&self, a: GetByIdArgs) -> Result<Outcome<Repo>> {
292 Ok(self
Members can read a private repository's pull request forks293 .visible(self.registry.by_id(&a.id).await?, &a.viewer)
Rust repos service with shipping; pull requests kept in the model294 .await?
295 .map_or_else(not_found, Outcome::Ok))
296 }
297
Agents as a team: lifecycle, merge queue, billing and a new shell298 async fn update(&self, a: UpdateArgs) -> Result<Outcome<Repo>> {
299 let viewer = Some(a.actor.clone());
300 let Some(repo) = self.readable(&a.path, &viewer).await? else {
301 return Ok(not_found());
302 };
Merge main (membership, two-factor, GitHub repo roles) into tokens303 // Its details take Maintain; its protection, Admin; who can see it,
304 // 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 look305 let protection_changes = a.protected.is_some_and(|protected| protected != repo.protected);
306 let details_change = a.description.is_some() || a.website.is_some() || a.topics.is_some();
307 let mut needed = Vec::new();
308 if details_change || !protection_changes {
309 needed.push(Capability::ManageSettings);
310 }
311 if protection_changes {
312 needed.push(Capability::ManageProtection);
313 }
314 let full_name = format!("{}/{}", repo.namespace, repo.name);
315 if repo.fork_of.is_some() {
316 return Ok(Outcome::fail(FailureCode::Forbidden, access::needs(Capability::ManageSettings, &full_name)));
317 }
318 if let Some(missing) = needed.into_iter().find(|capability| !registry::can(&repo, &viewer, *capability)) {
319 return Ok(Outcome::fail(FailureCode::Forbidden, access::needs(missing, &full_name)));
Agents as a team: lifecycle, merge queue, billing and a new shell320 }
321 if !a.actor.verified {
322 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
323 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look324 if let Some((code, message)) = lifecycle::archived_refusal(&repo) {
325 return Ok(Outcome::fail(code, message));
326 }
Agents as a team: lifecycle, merge queue, billing and a new shell327 let description = match a.description {
328 Some(text) => Some(
329 text.trim()
330 .chars()
331 .take(MAX_DESCRIPTION_CHARS)
332 .collect::<String>(),
333 )
334 .filter(|text| !text.is_empty()),
335 None => repo.description.clone(),
336 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look337 let website = match a.website.as_deref() {
338 Some(text) => match clean_website(text) {
339 Ok(website) => website,
340 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
341 },
342 None => repo.website.clone(),
343 };
344 // Who can see it is an owner's to change, and a free workspace's
345 // storage may not take it private: see lifecycle.rs.
346 let wants_private = a.is_private.filter(|private| *private != repo.is_private);
347 if wants_private.is_some()
348 && let Err((code, message)) = lifecycle::admin_only(
349 lifecycle::Asker::on(&a.actor, &repo),
350 &repo.namespace,
351 "change the visibility of",
Merge main (membership, two-factor, GitHub repo roles) into tokens352 Capability::ChangeVisibility,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look353 )
354 {
355 return Ok(Outcome::fail(code, message));
356 }
Merge main (membership, two-factor, GitHub repo roles) into tokens357 if let Some(private) = wants_private
358 && let Some(why) = lifecycle::visibility_refusal(&a.actor, &repo, private)
359 {
360 return Ok(Outcome::fail(FailureCode::Forbidden, why));
361 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look362 let is_private = repo.is_private;
Agents as a team: lifecycle, merge queue, billing and a new shell363 let protected = a.protected.unwrap_or(repo.protected);
Search across all of g1t, Explore, and a command palette364 let topics = match &a.topics {
365 Some(topics) => match clean_topics(topics) {
366 Ok(topics) => topics,
367 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
368 },
369 None => repo.topics.clone(),
370 };
Agents as a team: lifecycle, merge queue, billing and a new shell371 self.registry
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look372 .update(&repo.id, description.as_deref(), protected, &topics, website.as_deref())
Agents as a team: lifecycle, merge queue, billing and a new shell373 .await?;
Search across all of g1t, Explore, and a command palette374 let updated = Repo {
Agents as a team: lifecycle, merge queue, billing and a new shell375 description,
376 is_private,
377 protected,
Search across all of g1t, Explore, and a command palette378 topics,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look379 website,
Agents as a team: lifecycle, merge queue, billing and a new shell380 ..repo
Search across all of g1t, Explore, and a command palette381 };
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge382 // Whether the default branch takes only pull requests is now its
383 // branch protection ruleset's to say (work's rulesets.rs).
384 if let (Some(protected), Some(work)) = (a.protected, &self.work) {
385 #[derive(Serialize)]
386 struct RequirePullRequest<'a> {
387 repo: &'a Repo,
388 protected: bool,
389 actor: &'a User,
390 }
391 let set: Result<Outcome<bool>> =
392 g1t_kit::call(work, "set_requires_pull_request", &RequirePullRequest { repo: &updated, protected, actor: &a.actor }).await;
393 match set {
394 Ok(Outcome::Ok(_)) => {}
395 Ok(Outcome::Fail(failure)) => return Ok(Outcome::Fail(failure)),
396 Err(error) => return Err(error),
397 }
398 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look399 if let Some(private) = wants_private {
400 return self.change_visibility(updated, private, &a.actor, a.surface).await;
401 }
402 let visibility_changed = false;
Search across all of g1t, Explore, and a command palette403 // Search and anything else that shows the repository hears of it;
404 // a change of visibility is announced on its own as well, so that
405 // what was public stops being shown at once.
406 self.publish(NewEvent {
407 kind: "repo.updated",
408 source: SOURCE,
409 repo_id: Some(updated.id.clone()),
410 actor: Some(a.actor.id.clone()),
411 data: RepoUpdated {
412 repo_id: updated.id.clone(),
413 namespace: updated.namespace.clone(),
414 name: updated.name.clone(),
415 is_private,
416 visibility_changed,
417 },
418 })
419 .await?;
420 Ok(Outcome::Ok(updated))
421 }
422
423 /// The repository with this id, if it is not a fork, and its store.
424 async fn stored(&self, repo_id: &str) -> Result<Option<S::Repo>> {
425 match self.registry.by_id(repo_id).await? {
426 Some(repo) if repo.fork_of.is_none() => Ok(Some(self.store.open(&store_key(&repo)).await?)),
427 _ => Ok(None),
428 }
429 }
430
431 async fn list_files(&self, a: ListFilesArgs) -> Result<FileList> {
432 let Some(repo) = self.registry.by_id(&a.repo_id).await?.filter(|repo| repo.fork_of.is_none()) else {
433 return Ok(FileList::default());
434 };
435 let git = self.store.open(&store_key(&repo)).await?;
436 let head = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
437 listing::list(&git, None, &head, &a.skip_dirs, a.limit).await
438 }
439
440 async fn changed_files(&self, a: ChangedFilesArgs) -> Result<FileList> {
441 let Some(git) = self.stored(&a.repo_id).await? else {
442 return Ok(FileList::default());
443 };
444 listing::list(&git, a.base.as_deref(), &a.head, &a.skip_dirs, a.limit).await
445 }
446
Composer from the workspace's own repositories, and go get from g1t.sh447 /// Branches and tags with their commits, for g1t's own services.
448 async fn refs_of(&self, a: RefsArgs) -> Result<Option<RepoRefs>> {
449 let Some(repo) = self.registry.by_id(&a.repo_id).await?.filter(|repo| repo.fork_of.is_none()) else {
450 return Ok(None);
451 };
452 let git = self.store.open(&store_key(&repo)).await?;
453 let access = git.access(Scope::Read).await?;
454 let refs = refs::heads_and_tags(refs::all(&access).await?)
455 .into_iter()
456 .map(|(name, commit)| GitRefEntry { name, commit })
457 .collect();
458 Ok(Some(RepoRefs { repo, refs }))
459 }
460
461 async fn raw_file(&self, a: RawFileArgs) -> Result<Option<RawFile>> {
462 use base64::Engine;
463 let Some(git) = self.stored(&a.repo_id).await? else {
464 return Ok(None);
465 };
466 Ok(git
467 .read_file(&a.git_ref, &a.path)
468 .await?
469 .filter(|bytes| bytes.len() <= a.max_bytes as usize)
470 .map(|bytes| RawFile { size: bytes.len() as u64, data: base64::engine::general_purpose::STANDARD.encode(bytes) }))
471 }
472
473 async fn raw_blobs(&self, a: RawBlobsArgs) -> Result<Vec<RawBlob>> {
474 use base64::Engine;
475 let Some(git) = self.stored(&a.repo_id).await? else {
476 return Ok(Vec::new());
477 };
478 let hashes: Vec<&String> = a.hashes.iter().take(MAX_READ_BLOBS).collect();
479 let mut out = Vec::with_capacity(hashes.len());
480 // A few at a time, as listing::read does: each is a round trip.
481 for group in hashes.chunks(8) {
482 let read = futures_util::future::try_join_all(group.iter().map(|hash| git.read_blob(hash))).await?;
483 for (hash, bytes) in group.iter().zip(read) {
484 let size = bytes.as_ref().map_or(0, |bytes| bytes.len() as u64);
485 let data = bytes
486 .filter(|bytes| bytes.len() <= a.max_bytes as usize)
487 .map(|bytes| base64::engine::general_purpose::STANDARD.encode(bytes));
488 out.push(RawBlob { hash: (*hash).clone(), size, data });
489 }
490 }
491 Ok(out)
492 }
493
Search across all of g1t, Explore, and a command palette494 async fn read_blobs(&self, a: ReadBlobsArgs) -> Result<Vec<BlobText>> {
495 let Some(git) = self.stored(&a.repo_id).await? else {
496 return Ok(Vec::new());
497 };
498 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 shell499 }
500
Rust repos service with shipping; pull requests kept in the model501 async fn create(&self, a: CreateArgs) -> Result<Outcome<Repo>> {
502 if !a.owner.verified {
503 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
504 }
505 let name = a.name.trim().to_lowercase();
506 if !is_valid_repo_name(&name) {
507 return Ok(Outcome::fail(
508 FailureCode::Invalid,
509 "Use letters, digits, dots, hyphens and underscores only.",
510 ));
511 }
Workspaces own repositories512 let namespace = a.namespace.trim().to_lowercase();
513 if namespace.is_empty() {
Rust repos service with shipping; pull requests kept in the model514 return Ok(Outcome::fail(
515 FailureCode::Invalid,
Workspaces own repositories516 "Say which workspace to create the repository in.",
517 ));
518 }
Merge main (membership, two-factor, GitHub repo roles) into tokens519 let Some(role) = a.owner.role_in(&namespace) else {
Workspaces own repositories520 return Ok(Outcome::fail(
521 FailureCode::Forbidden,
522 "You are not a member of that workspace.",
Rust repos service with shipping; pull requests kept in the model523 ));
Merge main (membership, two-factor, GitHub repo roles) into tokens524 };
525 // Who may create which: the workspace's member privileges. A
526 // workspace's own token acts as an owner would.
527 let role = if a.owner.kind == PrincipalKind::Workspace { Role::Owner } else { role };
528 if let Some(why) = a.owner.privileges_in(&namespace).creation_refusal(role, a.is_private, &namespace) {
529 return Ok(Outcome::fail(FailureCode::Forbidden, why));
Rust repos service with shipping; pull requests kept in the model530 }
Workspaces own repositories531 let path = RepoPath { namespace, name };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look532 match self.registry.by_path_any(&path).await? {
533 Some((_, None)) => {
534 return Ok(Outcome::fail(
535 FailureCode::Conflict,
536 "That workspace already has a repository with that name.",
537 ));
538 }
539 Some((_, Some(_))) => {
540 return Ok(Outcome::fail(
541 FailureCode::Conflict,
542 format!(
543 "{}/{} 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.",
544 path.namespace, path.name
545 ),
546 ));
547 }
548 None => {}
549 }
550 // With a credential (a GitHub App installation's token), everything
551 // is copied: every branch and tag. See mirror.rs.
552 let mut credentialed = None;
553 if let (Some(url), Some(token)) = (a.import_url.as_deref(), a.import_token.as_deref()) {
554 let Some(url) = import::clean_url(url) else {
555 return Ok(Outcome::fail(FailureCode::Invalid, "That is not an https repository address."));
556 };
557 let source = mirror::Endpoint::github(&url, token);
558 match mirror::probe(&source).await? {
559 Ok(advertised) => credentialed = Some((source, advertised)),
560 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
561 }
Rust repos service with shipping; pull requests kept in the model562 }
Agents as a team: lifecycle, merge queue, billing and a new shell563 // An import is fetched before anything is created, so that an
564 // address that does not work leaves nothing behind.
565 let mut imported = None;
566 if let Some(url) = a
567 .import_url
568 .as_deref()
569 .map(str::trim)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look570 .filter(|url| !url.is_empty() && credentialed.is_none())
Agents as a team: lifecycle, merge queue, billing and a new shell571 {
572 let Some(url) = import::clean_url(url) else {
573 return Ok(Outcome::fail(
574 FailureCode::Invalid,
575 "Give the https address of a public repository, such as https://github.com/owner/repo.",
576 ));
577 };
578 let remote = match import::discover(&url).await? {
579 Ok(remote) => remote,
580 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
581 };
A public import copies every branch and tag, so an imported library keeps its releases582 imported = Some((remote, url));
Agents as a team: lifecycle, merge queue, billing and a new shell583 }
Rust repos service with shipping; pull requests kept in the model584 let now = now_ms();
585 let repo = Repo {
586 id: new_id("rep", now),
587 namespace: path.namespace,
588 name: path.name,
589 description: a
590 .description
591 .map(|text| text.trim().to_owned())
592 .filter(|text| !text.is_empty()),
593 is_private: a.is_private,
594 owner_id: a.owner.id.clone(),
Agents as a team: lifecycle, merge queue, billing and a new shell595 default_branch: imported
596 .as_ref()
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look597 .map(|(remote, _)| remote.branch.clone())
598 .or_else(|| credentialed.as_ref().and_then(|(_, advertised)| advertised.default_branch()))
599 .unwrap_or_else(|| "main".to_owned()),
Rust repos service with shipping; pull requests kept in the model600 fork_of: None,
Agents as a team: lifecycle, merge queue, billing and a new shell601 protected: false,
RFC 3339 timestamps in identity and repos602 created_at: rfc3339(now),
Search across all of g1t, Explore, and a command palette603 topics: Vec::new(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look604 website: None,
605 archived_at: None,
Rust repos service with shipping; pull requests kept in the model606 };
Merge branch 'worktree-agent-a2013627e5ea4ab13'607 let namespace = match self.place(&repo).await? {
608 Ok(namespace) => namespace,
609 Err(unplaced) => return Ok(Outcome::fail(FailureCode::Conflict, unplaced.message())),
610 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily611 self.registry
612 .claim_store_key(&repo, namespace.as_deref(), &self.store.default_namespace())
613 .await?;
Rust repos service with shipping; pull requests kept in the model614 self.store
615 .create(
616 &store_key(&repo),
617 repo.description.as_deref(),
618 &repo.default_branch,
619 )
620 .await?;
621 self.registry.insert(&repo).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look622 // A repository that was transferred away from this path stops
623 // redirecting here.
624 self.registry
625 .drop_redirect(&RepoPath {
626 namespace: repo.namespace.clone(),
627 name: repo.name.clone(),
628 })
629 .await?;
A public import copies every branch and tag, so an imported library keeps its releases630 // Every branch and tag the import made, announced as pushes.
631 let mut pushed: Vec<(String, String)> = Vec::new();
632 // A public repository, read with no credential: every branch and
633 // tag is copied too, the default branch the one its HEAD names.
634 if let Some((_, url)) = imported {
Agents as a team: lifecycle, merge queue, billing and a new shell635 let access = self
636 .store
637 .open(&store_key(&repo))
638 .await?
639 .access(Scope::Write)
640 .await?;
A public import copies every branch and tag, so an imported library keeps its releases641 let target = mirror::Endpoint::bearer(&access.remote, &access.token);
642 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 200ms643 self.refs_moved(&repo.id).await;
A public import copies every branch and tag, so an imported library keeps its releases644 match copied {
645 Ok(copied) => pushed = mirror::import_pushes(&copied.updated, &repo.default_branch),
646 Err(reason) => {
647 self.registry.remove(&repo.id).await?;
648 return Ok(Outcome::fail(
649 FailureCode::Invalid,
650 format!("The repository could not be stored: {reason}"),
651 ));
652 }
Agents as a team: lifecycle, merge queue, billing and a new shell653 }
654 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look655 if let Some((source, _)) = credentialed {
656 let access = self
657 .store
658 .open(&store_key(&repo))
659 .await?
660 .access(Scope::Write)
661 .await?;
662 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 200ms663 let copied = mirror::copy(&source, &target, mirror::Prune::Yes).await?;
664 self.refs_moved(&repo.id).await;
665 match copied {
A public import copies every branch and tag, so an imported library keeps its releases666 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 look667 Err(reason) => {
668 self.registry.remove(&repo.id).await?;
669 return Ok(Outcome::fail(
670 FailureCode::Invalid,
671 format!("The repository could not be copied: {reason}"),
672 ));
673 }
674 }
675 }
Merge main (membership, two-factor, GitHub repo roles) into tokens676 // Whoever creates a repository is an Admin of it, as a role given
677 // to them on it, whatever the workspace's base permission.
678 if a.owner.kind == PrincipalKind::User
679 && let Some(identity) = &self.identity
680 {
681 let granted: Result<bool> = g1t_kit::call(
682 identity,
683 "grant_creator",
684 &g1t_contracts::members::GrantCreatorArgs {
685 repo_id: repo.id.clone(),
686 namespace: repo.namespace.clone(),
687 name: repo.name.clone(),
688 user_id: a.owner.id.clone(),
689 },
690 )
691 .await;
692 if let Err(error) = granted {
693 worker::console_error!("creator of {} not given Admin: {error}", repo.id);
694 }
695 }
Rust repos service with shipping; pull requests kept in the model696 self.publish(NewEvent {
697 kind: "repo.created",
698 source: SOURCE,
699 repo_id: Some(repo.id.clone()),
700 actor: Some(a.owner.id),
701 data: RepoCreated {
702 repo_id: repo.id.clone(),
703 namespace: repo.namespace.clone(),
704 name: repo.name.clone(),
705 is_private: repo.is_private,
706 },
707 })
708 .await?;
A public import copies every branch and tag, so an imported library keeps its releases709 for (git_ref, head) in &pushed {
710 self.publish_push(&repo, git_ref, None, head, None).await?;
Agents as a team: lifecycle, merge queue, billing and a new shell711 }
Rust repos service with shipping; pull requests kept in the model712 Ok(Outcome::Ok(repo))
713 }
714
Merge branch 'worktree-agent-a2013627e5ea4ab13'715 /// Where a workspace keeps its data, asked of identity only when an EU
716 /// namespace is configured: without one, every workspace's
717 /// repositories go anywhere and identity is never asked.
718 async fn residency_of(&self, workspace: &str) -> Result<shards::Residency> {
719 if self.placement.eu.is_none() {
720 return Ok(shards::Residency::Anywhere);
721 }
722 let Some(identity) = &self.identity else {
723 return Ok(shards::Residency::Anywhere);
724 };
725 let residency: Option<g1t_contracts::identity::DataResidency> = g1t_kit::call(
726 identity,
727 "workspace_residency",
728 &g1t_contracts::identity::SlugArgs { slug: workspace.to_owned() },
729 )
730 .await?;
731 Ok(match residency {
732 Some(g1t_contracts::identity::DataResidency::Eu) => shards::Residency::Eu,
733 _ => shards::Residency::Anywhere,
734 })
735 }
736
737 /// How each bound namespace stands (namespaces.rs).
738 async fn standings(&self) -> Result<Vec<namespaces::Standing>> {
739 let bound = self.store.namespaces();
740 let default = self.store.default_namespace();
741 let now = now_ms();
742 let config = namespaces::Configured {
743 bound: &bound,
744 default: &default,
745 placement: &self.placement,
746 limits: &self.limits,
747 on_fallback: &|namespace| self.store.on_fallback(&shards::compose(Some(namespace), "x", &default)),
748 writable: &|namespace| self.store.writable(namespace),
749 breaker_open: &|namespace| resilience::open_now(namespace, now),
750 };
751 let (held, recent) = futures_util::future::join(namespaces::held(&self.registry.db), namespaces::recent(&self.registry.db, now)).await;
752 Ok(namespaces::standings(&config, &held?, &recent?))
753 }
754
755 /// The namespace a new repository goes in (shards.rs): its workspace's
756 /// residency, then how each namespace stands, read only when there is
757 /// a choice to make. `Ok(None)` for the default.
758 async fn place(&self, repo: &Repo) -> Result<std::result::Result<Option<String>, shards::Unplaced>> {
759 let residency = self.residency_of(&repo.namespace).await?;
760 let bound = self.store.namespaces();
761 let loads = if residency == shards::Residency::Anywhere && !self.placement.needs_loads(&bound) {
762 // One namespace to choose from at most: nothing to read.
763 bound
764 .iter()
765 .map(|namespace| shards::Load {
766 namespace: namespace.clone(),
767 bound: true,
768 writable: self.store.writable(namespace),
769 ..shards::Load::default()
770 })
771 .collect()
772 } else {
773 let default = self.store.default_namespace();
774 let now = now_ms();
775 let config = namespaces::Configured {
776 bound: &bound,
777 default: &default,
778 placement: &self.placement,
779 limits: &self.limits,
780 on_fallback: &|namespace| self.store.on_fallback(&shards::compose(Some(namespace), "x", &default)),
781 writable: &|namespace| self.store.writable(namespace),
782 breaker_open: &|namespace| resilience::open_now(namespace, now),
783 };
784 namespaces::loads(&self.registry.db, &config, now).await?
785 };
786 Ok(self.placement.choose(&repo.id, residency, &loads))
787 }
788
789 /// `storage_options`: what a workspace may choose about where its
790 /// repositories are kept.
791 fn storage_options(&self) -> StorageOptions {
792 let bound = self.store.namespaces();
793 StorageOptions { eu_available: self.placement.eu_available(&bound, |namespace| self.store.writable(namespace)) }
794 }
795
Rust repos service with shipping; pull requests kept in the model796 async fn tree(&self, a: TreeArgs) -> Result<Outcome<TreeView>> {
797 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
798 return Ok(not_found());
799 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily800 let git = self.read_git(&repo).await?;
Rust repos service with shipping; pull requests kept in the model801 let git_ref = a
802 .git_ref
803 .clone()
804 .unwrap_or_else(|| repo.default_branch.clone());
805
806 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
807 // An unknown ref is an error; a repo with no commits is just empty.
808 if a.git_ref.is_some() {
809 return Ok(Outcome::fail(
810 FailureCode::NotFound,
811 "No such branch, tag or commit.",
812 ));
813 }
814 return Ok(Outcome::Ok(TreeView {
815 repo,
816 git_ref,
817 path: a.tree_path,
818 head: None,
819 entries: Vec::new(),
820 readme: None,
821 }));
822 };
823
824 let no_directory = || Outcome::fail(FailureCode::NotFound, "No such directory.");
825 let mut entries = git.read_tree(&head.tree_hash).await?;
826 for segment in a.tree_path.split('/').filter(|segment| !segment.is_empty()) {
827 let next = entries.as_ref().and_then(|entries| {
828 entries
829 .iter()
830 .find(|entry| entry.name == segment && entry.kind == EntryKind::Tree)
831 });
832 let Some(next) = next else {
833 return Ok(no_directory());
834 };
835 entries = git.read_tree(&next.hash).await?;
836 }
837 let Some(mut entries) = entries else {
838 return Ok(no_directory());
839 };
840 // Directories first, then by name.
841 entries.sort_by(|a, b| {
842 (b.kind == EntryKind::Tree)
843 .cmp(&(a.kind == EntryKind::Tree))
844 .then_with(|| a.name.cmp(&b.name))
845 });
846
847 let readme_entry = entries
848 .iter()
849 .find(|entry| entry.kind == EntryKind::Blob && is_readme(&entry.name));
850 let readme = match readme_entry {
851 Some(entry) => git.read_blob(&entry.hash).await?.map(|bytes| Readme {
852 name: entry.name.clone(),
853 text: text_of(bytes),
854 }),
855 None => None,
856 };
857 Ok(Outcome::Ok(TreeView {
858 repo,
859 git_ref,
860 path: a.tree_path,
861 head: Some(head),
862 entries,
863 readme,
864 }))
865 }
866
867 async fn blob(&self, a: BlobArgs) -> Result<Outcome<BlobView>> {
868 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
869 return Ok(not_found());
870 };
871 let bytes = if a.file_path.is_empty() {
872 None
873 } else {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily874 let git = self.read_git(&repo).await?;
Rust repos service with shipping; pull requests kept in the model875 git.read_file(&a.git_ref, &a.file_path).await?
876 };
877 let Some(bytes) = bytes else {
878 return Ok(Outcome::fail(FailureCode::NotFound, "No such file."));
879 };
880 Ok(Outcome::Ok(BlobView {
881 repo,
882 git_ref: a.git_ref,
883 path: a.file_path,
884 size: bytes.len() as u64,
885 text: text_of(bytes),
886 }))
887 }
888
Agents as a team: lifecycle, merge queue, billing and a new shell889 async fn blame(&self, a: BlameArgs) -> Result<Outcome<Blame>> {
890 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
891 return Ok(not_found());
892 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily893 let git = self.read_git(&repo).await?;
Agents as a team: lifecycle, merge queue, billing and a new shell894 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
895 Ok(match blame::blame(&git, &git_ref, &a.file_path).await? {
896 Some(blame) => Outcome::Ok(blame),
897 None => not_found(),
898 })
899 }
900
Rust repos service with shipping; pull requests kept in the model901 async fn log(&self, a: LogArgs) -> Result<Outcome<Vec<Commit>>> {
902 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
903 return Ok(not_found());
904 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily905 let git = self.read_git(&repo).await?;
Rust repos service with shipping; pull requests kept in the model906 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
907 Ok(Outcome::Ok(git.log(&git_ref, a.limit).await?))
908 }
909
Merge last commits over the whole history: progress kept per ref and path, a push reads only its own commits910 /// Which commit last changed each entry of a directory. A finished
911 /// answer is kept in this colo's cache by repository, head commit and
912 /// path: a commit's history never changes, so it is good for as long as
913 /// it is kept. Every walk also keeps its progress by ref and path
914 /// (last_commits.rs), so the next call goes on from it: from where it
915 /// stopped, or for a new head, only back to the old one.
Branches and Tags pages, each file's last commit, and the branch menu on files916 async fn last_commits(&self, a: g1t_contracts::repos::LastCommitsArgs) -> Result<Outcome<g1t_contracts::repos::LastCommits>> {
917 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
918 return Ok(not_found());
919 };
920 let git = self.read_git(&repo).await?;
921 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
922 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
923 return Ok(Outcome::fail(FailureCode::NotFound, "No such branch, tag or commit."));
924 };
Merge last commits over the whole history: progress kept per ref and path, a push reads only its own commits925 // v2: v1 kept walks cut short at 300 commits as if finished.
Branches and Tags pages, each file's last commit, and the branch menu on files926 let key = format!(
Merge last commits over the whole history: progress kept per ref and path, a push reads only its own commits927 "https://last-commits.g1t.internal/v2/{}/{}/{}",
Branches and Tags pages, each file's last commit, and the branch menu on files928 repo.id,
929 head.hash,
930 a.tree_path.split('/').map(urlencoding_segment).collect::<Vec<_>>().join("/")
931 );
932 let cache = worker::Cache::default();
933 if let Ok(Some(mut kept)) = cache.get(key.as_str(), false).await {
934 if let Ok(found) = kept.json::<g1t_contracts::repos::LastCommits>().await {
935 return Ok(Outcome::Ok(found));
936 }
937 }
Merge last commits over the whole history: progress kept per ref and path, a push reads only its own commits938 let memo = last_commits::Memo::new(&repo.id, &git_ref, &a.tree_path);
939 let shared = self.shared.as_deref();
940 let progress = memo.get(shared).await;
941 // Asked with a budget, the walk stops past it; without one, past
942 // LAST_COMMITS_WALK_MS, well before the caller's waitUntil ends.
A last-commits walk answers within 2.5 s with what it has, and keeps only finished answers943 let started = worker::Date::now().as_millis();
Merge last commits over the whole history: progress kept per ref and path, a push reads only its own commits944 let budget = a.budget_ms.unwrap_or(LAST_COMMITS_WALK_MS);
945 let out_of_time = move || worker::Date::now().as_millis().saturating_sub(started) > budget;
946 let walk = last_commits::last_commits(&git, &head.hash, &a.tree_path, progress, &out_of_time, last_commits::MAX_READS).await?;
947 if walk.reads > 0 {
948 memo.keep(shared, &walk.progress).await;
949 }
950 let settled = walk.progress.settled();
951 let found = g1t_contracts::repos::LastCommits { complete: walk.progress.complete(), entries: walk.progress.found };
952 if !settled {
A last-commits walk answers within 2.5 s with what it has, and keeps only finished answers953 return Ok(Outcome::Ok(found));
954 }
Branches and Tags pages, each file's last commit, and the branch menu on files955 if let Ok(mut response) = worker::Response::from_json(&found) {
956 let _ = response.headers_mut().set("cache-control", "max-age=604800");
957 let _ = cache.put(key.as_str(), response).await;
958 }
959 Ok(Outcome::Ok(found))
960 }
961
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25962 /// How far each branch head has moved from the default branch's head,
Merge branch drift: count across merges the way git does; v2 cache key963 /// in one call (drift.rs). Each count is kept in this colo's cache by
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25964 /// repository and the pair of hashes, for good: neither history can
Merge branch drift: count across merges the way git does; v2 cache key965 /// change. A head that moved is the only one walked. A failed read is
966 /// not kept, and "too far to count" only for a day.
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25967 async fn branch_drift(&self, a: BranchDriftArgs) -> Result<Outcome<BranchDrifts>> {
968 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
969 return Ok(not_found());
970 };
971 if !store::is_commit_hash(&a.base) {
972 return Ok(Outcome::fail(FailureCode::Invalid, "The default branch's head is a full commit hash."));
973 }
974 let heads: Vec<String> = a.heads.into_iter().take(MAX_DRIFT_HEADS).collect();
975 let git = self.read_git(&repo).await?;
Merge branch drift: count across merges the way git does; v2 cache key976 // v2: answers kept before 2026-10-09 said "no count" for every
977 // branch whose default branch took a merge since it left (drift.rs).
978 let key = |head: &str| format!("https://drift.g1t.internal/v2/{}/{}/{head}", repo.id, a.base);
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25979 let cache = worker::Cache::default();
980 let (base, kept) = futures_util::future::join(
981 git.log(&a.base, 1),
982 futures_util::future::join_all(heads.iter().map(|head| {
983 let (cache, url) = (&cache, key(head));
984 async move {
985 if !store::is_commit_hash(head) {
986 return None;
987 }
988 let mut found = cache.get(url.as_str(), false).await.ok()??;
989 found.json::<BranchDrift>().await.ok()
990 }
991 })),
992 )
993 .await;
994 let missing: Vec<String> = heads
995 .iter()
996 .zip(&kept)
997 .filter(|(head, kept)| kept.is_none() && store::is_commit_hash(head))
998 .map(|(head, _)| head.clone())
999 .collect();
1000 let measured = drift::measure(&git, &a.base, &missing).await;
1001 let mut fresh: HashMap<String, BranchDrift> = HashMap::new();
1002 for (head, found) in missing.into_iter().zip(measured) {
1003 let answer = BranchDrift { head: head.clone(), commit: found.commit, drift: found.drift };
1004 if found.settled
1005 && let Ok(mut response) = worker::Response::from_json(&answer)
1006 {
Merge branch drift: count across merges the way git does; v2 cache key1007 // A count is kept for good; "too far to count" for a day, so
1008 // a change to how far a walk goes reaches it.
1009 let max_age = if answer.drift.is_some() { "public, max-age=31536000, immutable" } else { "public, max-age=86400" };
1010 let _ = response.headers_mut().set("cache-control", max_age);
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 251011 let _ = cache.put(key(&head).as_str(), response).await;
1012 }
1013 fresh.insert(head, answer);
1014 }
1015 let branches = heads
1016 .iter()
1017 .zip(kept)
1018 .map(|(head, kept)| {
1019 kept.or_else(|| fresh.get(head).cloned())
1020 .unwrap_or_else(|| BranchDrift { head: head.clone(), commit: None, drift: None })
1021 })
1022 .collect();
1023 Ok(Outcome::Ok(BranchDrifts { base: base.ok().and_then(|log| log.into_iter().next()), branches }))
1024 }
1025
Branches and Tags pages, each file's last commit, and the branch menu on files1026 /// The repository's tags, newest commit first, at most 100.
1027 async fn tags(&self, a: g1t_contracts::repos::TagsArgs) -> Result<Outcome<Vec<g1t_contracts::repos::Tag>>> {
1028 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
1029 return Ok(not_found());
1030 };
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 251031 // Kept until the refs move, as the branch list is (store.rs): listing
1032 // the refs is a round trip to the store on every call otherwise.
1033 let version = refs_cache::usable(registry::refs_state(&repo.id), now_ms()).filter(|_| !self.store.on_fallback(&store_key(&repo)));
1034 let kept_at = version.map(|version| format!("https://tags.g1t.internal/{}/{version}", repo.id));
1035 if let Some(url) = &kept_at
1036 && let Ok(Some(mut kept)) = worker::Cache::default().get(url.as_str(), false).await
1037 && let Ok(tags) = kept.json::<Vec<g1t_contracts::repos::Tag>>().await
1038 {
1039 return Ok(Outcome::Ok(tags));
1040 }
1041 let (tags, complete) = self.read_tags(&repo).await?;
1042 if complete
1043 && let Some(url) = &kept_at
1044 && let Ok(mut response) = worker::Response::from_json(&tags)
1045 {
1046 let _ = response.headers_mut().set("cache-control", "public, max-age=300");
1047 let _ = worker::Cache::default().put(url.as_str(), response).await;
1048 }
1049 Ok(Outcome::Ok(tags))
1050 }
1051
1052 /// The tags, and whether every one's commit was read (only then kept).
1053 async fn read_tags(&self, repo: &Repo) -> Result<(Vec<g1t_contracts::repos::Tag>, bool)> {
1054 let git = self.store.open(&store_key(repo)).await?;
Branches and Tags pages, each file's last commit, and the branch menu on files1055 let access = git.access(Scope::Read).await?;
1056 let named: Vec<(String, String)> = refs::heads_and_tags(refs::all(&access).await?)
1057 .into_iter()
1058 .filter_map(|(name, hash)| name.strip_prefix("refs/tags/").map(|tag| (tag.to_owned(), hash)))
1059 .collect();
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 251060 let read = self.read_git(repo).await?;
Branches and Tags pages, each file's last commit, and the branch menu on files1061 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 251062 let complete = commits.iter().all(Result::is_ok);
Branches and Tags pages, each file's last commit, and the branch menu on files1063 let mut tags: Vec<g1t_contracts::repos::Tag> = named
1064 .into_iter()
1065 .zip(commits.into_iter().map(|found| found.ok().and_then(|list| list.into_iter().next())).chain(std::iter::repeat(None)))
1066 .map(|((name, _), commit)| g1t_contracts::repos::Tag { name, commit })
1067 .collect();
1068 tags.sort_by(|a, b| {
1069 let at = |tag: &g1t_contracts::repos::Tag| tag.commit.as_ref().map(|c| c.authored_at.clone()).unwrap_or_default();
1070 at(b).cmp(&at(a)).then_with(|| b.name.cmp(&a.name))
1071 });
1072 tags.truncate(MAX_TAGS_READ);
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 251073 Ok((tags, complete))
Branches and Tags pages, each file's last commit, and the branch menu on files1074 }
1075
Pull requests from branches1076 /// The repository's branches, default branch first.
1077 async fn branches(&self, a: BranchesArgs) -> Result<Outcome<Vec<Branch>>> {
1078 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
1079 return Ok(not_found());
1080 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1081 let mut branches = self.read_git(&repo).await?.branches().await?;
Pull requests from branches1082 branches.sort_by_key(|branch| branch.name != repo.default_branch);
1083 Ok(Outcome::Ok(branches))
1084 }
1085
Agents as a team: lifecycle, merge queue, billing and a new shell1086 /// Whether a pull request's source lacks commits that the branch it
1087 /// would merge into has.
1088 async fn behind(&self, a: BehindArgs) -> Result<bool> {
1089 let Some(source) = self.registry.by_id(&a.source_id).await? else {
1090 return Ok(false);
1091 };
1092 let target = match &source.fork_of {
1093 Some(id) => self.registry.by_id(id).await?,
1094 None => Some(source.clone()),
1095 };
1096 let Some(target) = target else {
1097 return Ok(false);
1098 };
1099 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 bar1100 let target_branch = a.target_branch.unwrap_or_else(|| target.default_branch.clone());
Agents as a team: lifecycle, merge queue, billing and a new shell1101 let target_head = self
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1102 .read_git(&target)
Agents as a team: lifecycle, merge queue, billing and a new shell1103 .await?
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1104 .log(&target_branch, 1)
Agents as a team: lifecycle, merge queue, billing and a new shell1105 .await?
1106 .into_iter()
1107 .next()
1108 .map(|commit| commit.hash);
1109 let Some(target_head) = target_head else {
1110 return Ok(false);
1111 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1112 let source_git = self.read_git(&source).await?;
Agents as a team: lifecycle, merge queue, billing and a new shell1113 let history = source_git.log(&branch, MAX_ANCESTRY).await?;
1114 if history.is_empty() {
1115 return Ok(false);
1116 }
1117 Ok(!descends_from(&source_git, &history, &target_head).await?)
1118 }
1119
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1120 /// The files a pull request's source and the default branch it would
1121 /// merge into each changed since they last agreed. Where the two lists
1122 /// share no file, the merge cannot conflict; where they do, it may.
1123 async fn divergence(&self, a: BehindArgs) -> Result<Option<Divergence>> {
1124 let Some(source) = self.registry.by_id(&a.source_id).await? else {
1125 return Ok(None);
1126 };
1127 let target = match &source.fork_of {
1128 Some(id) => self.registry.by_id(id).await?,
1129 None => Some(source.clone()),
1130 };
1131 let Some(target) = target else {
1132 return Ok(None);
1133 };
1134 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 bar1135 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 daily1136 let source_git = self.read_git(&source).await?;
1137 let target_git = self.read_git(&target).await?;
1138 // The target's side is the same for every pull request into it, and
1139 // worked out once per head (coalesce.rs).
1140 let (history, side) = futures_util::future::try_join(
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1141 source_git.log(&branch, MAX_ANCESTRY),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1142 self.target_side(&target, &target_git, &target_branch),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1143 )
1144 .await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1145 let target_history = &side.history;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1146 let (Some(head), Some(base)) = (history.first(), target_history.first()) else {
1147 return Ok(None);
1148 };
1149 let behind = !descends_from(&source_git, &history, &base.hash).await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1150 let merge_base = nearest_ancestor_in(&source_git, &history, &side.shared).await?;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1151 let mut divergence = Divergence {
1152 head: head.hash.clone(),
1153 base: base.hash.clone(),
1154 merge_base: merge_base.clone(),
1155 behind,
1156 ..Divergence::default()
1157 };
1158 let merge_base_tree = match &merge_base {
1159 Some(hash) => target_history
1160 .iter()
1161 .find(|commit| commit.hash == *hash)
1162 .map(|commit| commit.tree_hash.clone()),
1163 None => None,
1164 };
1165 let Some(merge_base_tree) = merge_base_tree else {
1166 // No common history to compare from: say nothing is known.
1167 divergence.truncated = true;
1168 return Ok(Some(divergence));
1169 };
1170 let (ours, truncated_ours) =
1171 diff::changed_paths(&source_git, Some(&merge_base_tree), &head.tree_hash).await?;
1172 divergence.ours = ours;
1173 divergence.truncated = truncated_ours;
1174 if behind {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1175 let now = now_ms();
1176 let key = (target.id.clone(), merge_base_tree.clone(), base.tree_hash.clone());
1177 let (theirs, truncated_theirs) = match THEIRS.with(|memo| memo.borrow().get(&key, now)) {
1178 Some(kept) => kept,
1179 None => {
1180 let found = diff::changed_paths(&target_git, Some(&merge_base_tree), &base.tree_hash).await?;
1181 THEIRS.with(|memo| memo.borrow_mut().put(key, found.clone(), now));
1182 found
1183 }
1184 };
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1185 divergence.theirs = theirs;
1186 divergence.truncated |= truncated_theirs;
1187 }
1188 Ok(Some(divergence))
1189 }
1190
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1191 /// A target branch's history from its head, worked out once per head
1192 /// for every pull request asking about it (coalesce.rs). The head is
1193 /// read under the refs version; the history by its hash, which the
1194 /// object cache keeps for good.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1195 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 daily1196 let now = now_ms();
1197 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 bar1198 .map(|version| (target.id.clone(), branch.to_owned(), version));
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1199 if let Some(key) = &key
1200 && let Some(side) = TARGETS.with(|memo| memo.borrow().get(key, now))
1201 {
1202 return Ok(side);
1203 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1204 let history = match git.log(branch, 1).await?.first() {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1205 Some(head) => git.log(&head.hash, MAX_ANCESTRY).await?,
1206 None => Vec::new(),
1207 };
1208 let side = coalesce::TargetSide::new(history);
1209 if let Some(key) = key {
1210 TARGETS.with(|memo| memo.borrow_mut().put(key, side.clone(), now));
1211 }
1212 Ok(side)
1213 }
1214
Pull requests from branches1215 async fn head(&self, a: HeadArgs) -> Result<Option<String>> {
1216 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
1217 return Ok(None);
1218 };
Workflows run when an agent's pull request is marked ready1219 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 daily1220 let git = self.read_git(&repo).await?;
Pull requests from branches1221 Ok(git
Workflows run when an agent's pull request is marked ready1222 .log(branch, 1)
Pull requests from branches1223 .await?
1224 .into_iter()
1225 .next()
1226 .map(|commit| commit.hash))
1227 }
1228
Merge queue: tested states are deleted once their entry leaves1229 async fn delete_branch(&self, a: DeleteBranchArgs) -> Result<Outcome<bool>> {
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches1230 if !deletable_branch(&a.branch, a.head.as_deref()) {
Merge queue: tested states are deleted once their entry leaves1231 return Ok(Outcome::fail(
1232 FailureCode::Forbidden,
1233 "Only branches g1t made for itself can be deleted this way.",
1234 ));
1235 }
1236 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
1237 return Ok(not_found());
1238 };
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches1239 if a.branch == repo.default_branch {
1240 return Ok(Outcome::fail(FailureCode::Forbidden, "The default branch is never deleted."));
1241 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'1242 let repo = match self.unpaused(repo).await? {
1243 Ok(repo) => repo,
1244 Err((code, message)) => return Ok(Outcome::fail(code, message)),
1245 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1246 self.live(&repo).await?;
Merge queue: tested states are deleted once their entry leaves1247 let git = self.store.open(&store_key(&repo)).await?;
1248 let Some(old) = git
1249 .branches()
1250 .await?
1251 .into_iter()
1252 .find(|branch| branch.name == a.branch)
1253 .map(|branch| branch.hash)
1254 else {
1255 return Ok(Outcome::Ok(false));
1256 };
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches1257 // Moved since the caller looked: someone else's commits are on it.
1258 if a.head.as_deref().is_some_and(|head| head != old) {
1259 return Ok(Outcome::fail(FailureCode::Conflict, format!("{} moved, so it was left alone.", a.branch)));
1260 }
Merge queue: tested states are deleted once their entry leaves1261 let access = git.access(Scope::Write).await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1262 let deleted = land::delete_ref(&access, &a.branch, &old).await?;
1263 self.refs_moved(&repo.id).await;
1264 if let Err(reason) = deleted {
Merge queue: tested states are deleted once their entry leaves1265 return Ok(Outcome::fail(
1266 FailureCode::Conflict,
1267 format!("{} could not be deleted: {reason}", a.branch),
1268 ));
1269 }
1270 Ok(Outcome::Ok(true))
1271 }
1272
Issues and pull requests replace intents and attempts1273 async fn fork_for_pull(&self, a: ForkArgs) -> Result<Outcome<Repo>> {
Rust repos service with shipping; pull requests kept in the model1274 let viewer = Some(a.actor.clone());
1275 let Some(source) = self
1276 .registry
1277 .by_id(&a.source_id)
1278 .await?
1279 .filter(|repo| can_read(repo, &viewer))
1280 else {
1281 return Ok(not_found());
1282 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1283 if let Some((code, message)) = lifecycle::archived_refusal(&source) {
1284 return Ok(Outcome::fail(code, message));
1285 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'1286 // Its working copy is made in its namespace: not while it moves.
1287 let source = match self.unpaused(source).await? {
1288 Ok(source) => source,
1289 Err((code, message)) => return Ok(Outcome::fail(code, message)),
1290 };
Rust repos service with shipping; pull requests kept in the model1291 let now = now_ms();
1292 let fork = Repo {
1293 id: new_id("rep", now),
Issues and pull requests replace intents and attempts1294 namespace: PULLS_NAMESPACE.to_owned(),
1295 name: a.pull_id.clone(),
Rust repos service with shipping; pull requests kept in the model1296 description: None,
1297 // A fork is exactly as visible as the repo it came from.
1298 is_private: source.is_private,
1299 owner_id: a.actor.id.clone(),
1300 default_branch: source.default_branch.clone(),
1301 fork_of: Some(source.id.clone()),
Agents as a team: lifecycle, merge queue, billing and a new shell1302 protected: false,
RFC 3339 timestamps in identity and repos1303 created_at: rfc3339(now),
Search across all of g1t, Explore, and a command palette1304 topics: Vec::new(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1305 website: None,
1306 archived_at: None,
Rust repos service with shipping; pull requests kept in the model1307 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1308 // Artifacts forks within a namespace: the copy goes where its
1309 // repository is.
1310 let (namespace, _) = store::locate(&store_key(&source));
1311 self.registry
1312 .claim_store_key(&fork, Some(&namespace), &self.store.default_namespace())
1313 .await?;
Rust repos service with shipping; pull requests kept in the model1314 self.store
1315 .open(&store_key(&source))
1316 .await?
1317 .fork(&store_key(&fork))
1318 .await?;
1319 self.registry.insert(&fork).await?;
1320 self.publish(NewEvent {
1321 kind: "repo.forked",
1322 source: SOURCE,
1323 repo_id: Some(source.id.clone()),
1324 actor: Some(a.actor.id),
1325 data: RepoForked {
1326 repo_id: fork.id.clone(),
1327 source_repo_id: source.id,
Issues and pull requests replace intents and attempts1328 pull_id: a.pull_id,
Rust repos service with shipping; pull requests kept in the model1329 },
1330 })
1331 .await?;
1332 Ok(Outcome::Ok(fork))
1333 }
1334
1335 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 200ms1336 let found = self.registry.by_path(&a.path).await?;
1337 Ok(match self.authorize_git(&a.path, &a.viewer, a.service, found).await? {
1338 Outcome::Ok(repo) => {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1339 self.live(&repo).await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1340 let write = a.service == GitService::ReceivePack;
1341 if write {
1342 // A push with this credential would not pass through
1343 // here, so nothing that lists the refs is kept until it
1344 // has expired (see refs_cache.rs).
1345 let until = now_ms() + store::CREDENTIAL_LIFE_MS + 60_000;
1346 if let Err(error) = self.registry.refs_open(&repo.id, until).await {
1347 // Before the column exists nothing is kept anyway.
1348 if registry::refs_state(&repo.id).is_some() {
1349 return Err(error);
1350 }
1351 }
1352 }
1353 let scope = if write { Scope::Write } else { Scope::Read };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1354 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 200ms1355 }
1356 Outcome::Fail(failure) => Outcome::Fail(failure),
1357 })
1358 }
1359
1360 /// The repository at `path` (`found`, as just read), if the viewer may
1361 /// use `service` on it: fetch from it, or push to it. A push to a path
1362 /// with nothing there makes the repository, in a workspace the pusher
1363 /// belongs to.
1364 async fn authorize_git(
1365 &self,
1366 path: &RepoPath,
1367 viewer: &Viewer,
1368 service: GitService,
1369 found: Option<Repo>,
1370 ) -> Result<Outcome<Repo>> {
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1371 let mut a = GitAccessArgs {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1372 path: path.clone(),
1373 viewer: viewer.clone(),
1374 service,
1375 };
Rust repos service with shipping; pull requests kept in the model1376 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 step1377 // An access token: pushing needs code:write, reading a private
1378 // repository code:read. A public repository reads as it would for
1379 // anyone. Which repositories a token reaches is its owner's, checked
1380 // below as for anyone.
1381 if let Some(access) = a.viewer.as_ref().and_then(|user| user.token.as_deref()).cloned() {
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1382 // A workflow job's token, and a deploy key, reach their own
1383 // repository only; a job's also the working copies of that
1384 // repository's pull requests, where their heads are.
1385 let name = format!("{}/{}", path.namespace, path.name);
1386 let source = match found.as_ref().and_then(|repo| repo.fork_of.as_deref()) {
1387 Some(source_id) if g1t_contracts::scopes::decide_repo(&access, &name).is_some() => self
1388 .registry
1389 .by_id(source_id)
1390 .await?
1391 .map(|source| format!("{}/{}", source.namespace, source.name)),
1392 _ => None,
1393 };
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1394 let public = found.as_ref().is_some_and(|repo| !repo.is_private);
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1395 if let Some(why) = git_token_refusal(&access, &name, source.as_deref(), write, public, found.is_some()) {
1396 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 step1397 }
1398 if !write && !access.allows(g1t_contracts::scopes::Scope::CodeRead) {
1399 a.viewer = None;
1400 }
1401 }
1402
Rust repos service with shipping; pull requests kept in the model1403 // Anonymous callers are asked to authenticate whether or not the repo
1404 // exists, so private repos cannot be told apart from missing ones.
1405 let denied = || match &a.viewer {
1406 Some(_) => not_found(),
1407 None => Outcome::fail(FailureCode::Unauthenticated, "Authentication required."),
1408 };
Agents as a team: lifecycle, merge queue, billing and a new shell1409 // An agent's token works through the API only: its sandbox has its
1410 // own way to push, to its own pull request.
1411 if a.viewer.as_ref().is_some_and(|user| user.kind == PrincipalKind::Agent) {
1412 return Ok(Outcome::fail(
1413 FailureCode::Forbidden,
1414 "A g1t agent's token cannot be used with git.",
1415 ));
1416 }
Rust repos service with shipping; pull requests kept in the model1417 if let (true, Some(user)) = (write, &a.viewer)
1418 && !user.verified
1419 {
1420 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
1421 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1422 let repo = match found {
Rust repos service with shipping; pull requests kept in the model1423 Some(repo) => {
1424 let allowed = if write {
1425 can_write(&repo, &a.viewer)
1426 } else {
Members can read a private repository's pull request forks1427 self.may_read(&repo, &a.viewer).await?
Rust repos service with shipping; pull requests kept in the model1428 };
1429 if !allowed {
1430 return Ok(denied());
1431 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1432 // An archived repository, or a pull request's copy of one,
1433 // is read-only.
1434 if write {
1435 let archived = match &repo.fork_of {
1436 Some(source) => self.registry.by_id(source).await?,
1437 None => Some(repo.clone()),
1438 };
1439 match archived {
1440 Some(source) => {
1441 if let Some((code, message)) = lifecycle::archived_refusal(&source) {
1442 return Ok(Outcome::fail(code, format!("{message}\n")));
1443 }
1444 }
1445 // The repository it was copied from is deleted.
1446 None => return Ok(denied()),
1447 }
1448 }
Rust repos service with shipping; pull requests kept in the model1449 repo
1450 }
1451 None => {
Workspaces own repositories1452 // Push to create, in a workspace the pusher belongs to.
Rust repos service with shipping; pull requests kept in the model1453 let owner = a
1454 .viewer
1455 .as_ref()
Workspaces own repositories1456 .filter(|user| write && user.is_member(&a.path.namespace.to_lowercase()));
Rust repos service with shipping; pull requests kept in the model1457 let Some(owner) = owner else {
1458 return Ok(denied());
1459 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1460 let created = self.create(push_to_create(owner, &a.path)).await?;
Rust repos service with shipping; pull requests kept in the model1461 match created {
1462 Outcome::Ok(repo) => repo,
1463 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
1464 }
1465 }
1466 };
Merge branch 'worktree-agent-a2013627e5ea4ab13'1467 // A push, or a credential to push with, waits while the repository
1468 // moves between namespaces (moves.rs), and goes to where it is now.
1469 if write {
1470 return Ok(match self.unpaused(repo).await? {
1471 Ok(repo) => Outcome::Ok(repo),
1472 Err((code, message)) => Outcome::fail(code, format!("{message}\n")),
1473 });
1474 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1475 Ok(Outcome::Ok(repo))
Rust repos service with shipping; pull requests kept in the model1476 }
1477
1478 async fn land(&self, a: LandArgs) -> Result<Outcome<Landed>> {
1479 let actor: Viewer = Some(a.actor.clone());
Pull requests from branches1480 let Some(source) = self.registry.by_id(&a.source_id).await? else {
Rust repos service with shipping; pull requests kept in the model1481 return Ok(not_found());
1482 };
Pull requests from branches1483 // A fork lands on the repository it came from; a branch on its own.
1484 let target = match &source.fork_of {
Rust repos service with shipping; pull requests kept in the model1485 Some(id) => self.registry.by_id(id).await?,
Pull requests from branches1486 None => Some(source.clone()),
Rust repos service with shipping; pull requests kept in the model1487 };
1488 let Some(target) = target.filter(|repo| can_read(repo, &actor)) else {
1489 return Ok(not_found());
1490 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1491 if !registry::can(&target, &actor, Capability::Merge) {
Rust repos service with shipping; pull requests kept in the model1492 return Ok(Outcome::fail(
1493 FailureCode::Forbidden,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1494 access::needs(Capability::Merge, &format!("{}/{}", target.namespace, target.name)),
Rust repos service with shipping; pull requests kept in the model1495 ));
1496 }
1497 if !a.actor.verified {
1498 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
1499 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1500 if let Some((code, message)) = lifecycle::archived_refusal(&target) {
1501 return Ok(Outcome::fail(code, message));
1502 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'1503 // Moving between namespaces: wait for it (moves.rs). Both are read
1504 // again once it is done, for their new keys.
1505 let (source, target) = match (self.unpaused(source).await?, self.unpaused(target).await?) {
1506 (Ok(source), Ok(target)) => (source, target),
1507 (Err((code, message)), _) | (_, Err((code, message))) => return Ok(Outcome::fail(code, message)),
1508 };
Rust repos service with shipping; pull requests kept in the model1509
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1510 let branch = &a.target_branch.clone().unwrap_or_else(|| target.default_branch.clone());
Pull requests from branches1511 let from_fork = source.id != target.id;
1512 let source_branch = match a.branch {
1513 Some(name) if !from_fork && name == *branch => {
1514 return Ok(Outcome::fail(
1515 FailureCode::Invalid,
1516 format!("{branch} cannot be merged into itself."),
1517 ));
1518 }
1519 Some(name) => name,
1520 None if from_fork => branch.clone(),
1521 None => {
1522 return Ok(Outcome::fail(
1523 FailureCode::Invalid,
1524 "Say which branch to merge.",
1525 ));
1526 }
1527 };
1528
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1529 self.live(&source).await?;
Pull requests from branches1530 let source_git = self.store.open(&store_key(&source)).await?;
Rust repos service with shipping; pull requests kept in the model1531 let target_git = self.store.open(&store_key(&target)).await?;
Pull requests from branches1532 let history = source_git.log(&source_branch, MAX_ANCESTRY).await?;
Rust repos service with shipping; pull requests kept in the model1533 let Some(new) = history.first().map(|commit| commit.hash.clone()) else {
1534 return Ok(Outcome::fail(
1535 FailureCode::Conflict,
Issues and pull requests replace intents and attempts1536 "This pull request has no commits to merge.",
Rust repos service with shipping; pull requests kept in the model1537 ));
1538 };
1539 let old = target_git
1540 .log(branch, 1)
1541 .await?
1542 .into_iter()
1543 .next()
1544 .map(|commit| commit.hash);
1545
1546 if old.as_deref() == Some(new.as_str()) {
Diffs on attempts; hosted agent presented as the g1t agent1547 return Ok(Outcome::Ok(Landed {
1548 commit: new,
1549 previous: None,
1550 }));
Rust repos service with shipping; pull requests kept in the model1551 }
1552 // Moving the branch to a commit that does not descend from its
1553 // current head would discard whatever landed in between.
1554 if let Some(old) = &old
Pull requests from branches1555 && !descends_from(&source_git, &history, old).await?
Rust repos service with shipping; pull requests kept in the model1556 {
Pull requests from branches1557 let remedy = if from_fork {
1558 format!("Pull {branch} into the pull request's fork, push, and merge again.")
1559 } else {
1560 format!("Merge {branch} into {source_branch}, push, and merge again.")
1561 };
Rust repos service with shipping; pull requests kept in the model1562 return Ok(Outcome::fail(
1563 FailureCode::Conflict,
Pull requests from branches1564 format!("{branch} has moved since this pull request was opened. {remedy}"),
Rust repos service with shipping; pull requests kept in the model1565 ));
1566 }
1567
Pull requests from branches1568 // For a branch the objects are already in the target; sending them
1569 // again is harmless and keeps one way of moving a ref.
1570 let source_access = source_git.access(Scope::Read).await?;
Rust repos service with shipping; pull requests kept in the model1571 let target_access = target_git.access(Scope::Write).await?;
1572 let pushed =
1573 land::fast_forward(&source_access, &target_access, branch, old.as_deref(), &new)
1574 .await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1575 self.refs_moved(&target.id).await;
Rust repos service with shipping; pull requests kept in the model1576 if let Err(reason) = pushed {
Issues and pull requests replace intents and attempts1577 // Most often another pull request landed between the check and the push.
Rust repos service with shipping; pull requests kept in the model1578 return Ok(Outcome::fail(
1579 FailureCode::Conflict,
1580 format!("{branch} could not be updated: {reason}"),
1581 ));
1582 }
GitHub Actions on g1t, part one: reading workflows1583 self.publish_push(
1584 &target,
1585 &format!("refs/heads/{branch}"),
1586 old.as_deref(),
1587 &new,
Merge branch 'worktree-agent-a3abfcce648e87dca'1588 Some(&a.actor),
GitHub Actions on g1t, part one: reading workflows1589 )
Events service in Rust, with RFC 3339 times and accurate push events1590 .await?;
Diffs on attempts; hosted agent presented as the g1t agent1591 Ok(Outcome::Ok(Landed {
1592 commit: new,
1593 previous: old,
1594 }))
1595 }
1596
1597 async fn compare(&self, a: CompareArgs) -> Result<Outcome<Comparison>> {
1598 let Some(repo) = self
Members can read a private repository's pull request forks1599 .visible(self.registry.by_id(&a.repo_id).await?, &a.viewer)
Diffs on attempts; hosted agent presented as the g1t agent1600 .await?
1601 else {
1602 return Ok(not_found());
1603 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1604 let git = self.read_git(&repo).await?;
Pull requests from branches1605 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 bar1606 // A pull request into another branch is compared from where it
1607 // left that branch.
1608 let base_branch = a.base_branch.clone();
Agents as a team: lifecycle, merge queue, billing and a new shell1609 // The head's history is only searched when the base is worked out
1610 // from another branch.
1611 let depth = if a.base.is_some() || is_commit_hash(head_ref) { 1 } else { MAX_ANCESTRY };
1612 let history = git.log(head_ref, depth).await?;
Diffs on attempts; hosted agent presented as the g1t agent1613 let Some(head) = history.first() else {
1614 return Ok(Outcome::fail(
1615 FailureCode::Conflict,
Pull requests from branches1616 "There are no commits to compare.",
Diffs on attempts; hosted agent presented as the g1t agent1617 ));
1618 };
1619
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1620 // Where the head's history meets the default branch of `against`,
1621 // or the branch asked for.
Pull requests from branches1622 let shared_with = async |against: &Repo| -> Result<Option<String>> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1623 let against_git = self.read_git(against).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1624 let branch = base_branch.as_deref().unwrap_or(&against.default_branch);
Pull requests from branches1625 let shared: HashSet<String> = against_git
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1626 .log(branch, MAX_ANCESTRY)
Pull requests from branches1627 .await?
1628 .into_iter()
1629 .map(|commit| commit.hash)
1630 .collect();
1631 nearest_ancestor_in(&git, &history, &shared).await
1632 };
Diffs on attempts; hosted agent presented as the g1t agent1633 let base = match (a.base, &repo.fork_of) {
1634 (Some(base), _) => Some(base),
1635 // A fork is compared with the last commit it shares with the
1636 // repository it came from.
1637 (None, Some(target_id)) => match self.registry.by_id(target_id).await? {
Pull requests from branches1638 Some(target) => shared_with(&target).await?,
Diffs on attempts; hosted agent presented as the g1t agent1639 None => None,
1640 },
Pull requests from branches1641 // A branch, with the point where it left the default branch.
Agents as a team: lifecycle, merge queue, billing and a new shell1642 // A single commit, with its first parent.
1643 (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 bar1644 (None, None) if head_ref != base_branch.as_deref().unwrap_or(&repo.default_branch) => {
1645 shared_with(&repo).await?
1646 }
Diffs on attempts; hosted agent presented as the g1t agent1647 (None, None) => head.parents.first().cloned(),
1648 };
1649 let base_tree = match &base {
1650 Some(base) => git
1651 .log(base, 1)
1652 .await?
1653 .into_iter()
1654 .next()
1655 .map(|commit| commit.tree_hash),
1656 None => None,
1657 };
1658 let (files, truncated) =
1659 diff::compare_trees(&git, base_tree.as_deref(), &head.tree_hash).await?;
1660 Ok(Outcome::Ok(Comparison {
1661 base,
1662 head: head.hash.clone(),
1663 files,
1664 truncated,
1665 }))
Rust repos service with shipping; pull requests kept in the model1666 }
1667
Merge branch 'worktree-agent-a3abfcce648e87dca'1668 /// Reports that `git_ref` of `repo` (a full ref) now points to `after`,
1669 /// moved by `actor` (marked when that was a workflow job's token).
Events service in Rust, with RFC 3339 times and accurate push events1670 async fn publish_push(
1671 &self,
1672 repo: &Repo,
GitHub Actions on g1t, part one: reading workflows1673 git_ref: &str,
1674 before: Option<&str>,
Events service in Rust, with RFC 3339 times and accurate push events1675 after: &str,
Merge branch 'worktree-agent-a3abfcce648e87dca'1676 actor: Option<&User>,
Events service in Rust, with RFC 3339 times and accurate push events1677 ) -> Result<()> {
Merge branch 'worktree-agent-a3abfcce648e87dca'1678 let caused_by_job = actor.and_then(g1t_contracts::events::job_run_of).map(str::to_owned);
1679 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 daily1680 }
1681
1682 /// `publish_push`, saying whether the push reached the store without
1683 /// being scanned for secrets first.
Merge branch 'worktree-agent-a3abfcce648e87dca'1684 #[allow(clippy::too_many_arguments)]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1685 async fn publish_git_push(
1686 &self,
1687 repo: &Repo,
1688 git_ref: &str,
1689 before: Option<&str>,
1690 after: &str,
1691 actor: Option<String>,
1692 unscanned: bool,
Merge branch 'worktree-agent-a3abfcce648e87dca'1693 caused_by_job: Option<String>,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1694 ) -> Result<()> {
Rust repos service with shipping; pull requests kept in the model1695 self.publish(NewEvent {
1696 kind: "git.push",
1697 source: SOURCE,
1698 repo_id: Some(repo.id.clone()),
1699 actor,
1700 data: GitPush {
1701 repo_id: repo.id.clone(),
GitHub Actions on g1t, part one: reading workflows1702 git_ref: git_ref.to_owned(),
1703 before: before.map(str::to_owned),
Rust repos service with shipping; pull requests kept in the model1704 after: after.to_owned(),
GitHub Actions on g1t, part one: reading workflows1705 default_branch: git_ref.strip_prefix("refs/heads/")
1706 == Some(repo.default_branch.as_str()),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1707 unscanned,
Merge branch 'worktree-agent-a3abfcce648e87dca'1708 caused_by_job,
Rust repos service with shipping; pull requests kept in the model1709 },
1710 })
1711 .await
1712 }
1713
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1714 /// Git over HTTPS. Only what decides the answer happens before it:
1715 /// the repository, who is asking and whether they may, the free
1716 /// workspace limits, push protection, and the store's own answer. The
1717 /// audit entry and what a push changed are recorded once git has its
1718 /// answer. Each answer says how long its steps took (`Server-Timing`).
1719 async fn git_http(&self, request: Request, env: &Env, ctx: &Context) -> Result<Response> {
1720 let mut timing = git_http::Timing::start();
Rust repos service with shipping; pull requests kept in the model1721 let Some(git) = git_http::parse(&request.url()?) else {
1722 return Response::error("Not found", 404);
1723 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1724 let response = match self.answer_git(request, &git, env, ctx, &mut timing).await {
1725 Ok(response) => response,
1726 // The git store is busy: git hears when to try again.
1727 Err(error) => match resilience::busy(&error.to_string()) {
1728 Some(busy) => git_http::busy_response(busy)?,
1729 None => return Err(error),
1730 },
1731 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1732 timing.apply(response)
1733 }
1734
1735 async fn answer_git(
1736 &self,
1737 request: Request,
1738 git: &git_http::GitRequest,
1739 env: &Env,
1740 ctx: &Context,
1741 timing: &mut git_http::Timing,
1742 ) -> Result<Response> {
1743 let write = git.service == GitService::ReceivePack;
1744 let get = request.method() == Method::Get;
1745 let identity = env.service("IDENTITY")?;
1746 // The repository and the caller's credentials, at once. A fetch may
1747 // go by the row as read a moment ago, for the same clone's next
1748 // request; a push always reads it. Anonymous callers cost nothing.
1749 let lookup = async {
1750 if write {
1751 self.registry.by_path(&git.path).await
1752 } else {
1753 self.registry.by_path_recent(&git.path).await
1754 }
1755 };
1756 let (found, viewer) =
1757 futures_util::future::join(lookup, git_http::viewer(&request, &identity)).await;
Merge branch 'worktree-agent-a8385d293d42c913a'1758 let mut found = found?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1759 timing.mark("repo");
Merge branch 'worktree-agent-a8385d293d42c913a'1760 // A workspace alias staff set (identity's aliases.rs: `g1t` for
1761 // `flagon-io`) is answered in place, as the repository under the
1762 // workspace's slug: pushes and some clients do not follow
1763 // redirects. Everything after this sees only the workspace's slug.
1764 let aliased = match found {
1765 Some(_) => None,
1766 None => git_http::aliased(git, &identity).await?,
1767 };
1768 if let Some(aliased) = &aliased {
1769 found = if write {
1770 self.registry.by_path(&aliased.path).await?
1771 } else {
1772 self.registry.by_path_recent(&aliased.path).await?
1773 };
1774 timing.mark("alias");
1775 }
1776 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 200ms1777 if found.is_none() {
1778 // A workspace that was renamed: git follows a redirect when it
1779 // first asks for refs, and uses the new address from then on.
1780 // A repository transferred to another workspace: the same, to
1781 // its new path. Fetches and pushes both follow either.
1782 let url = request.url()?;
1783 let (renamed, moved) = futures_util::future::join(
1784 git_http::renamed(&url, &identity),
1785 self.registry.resolve_moved(&git.path),
1786 )
1787 .await;
1788 timing.mark("moved");
1789 if let Some(location) = renamed? {
1790 return git_http::moved(&location, get);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1791 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1792 if let Some(now) = moved?
1793 && let Some(location) = git_http::transferred(&url, &now)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1794 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1795 return git_http::moved(&location, get);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1796 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1797 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1798 let viewer = viewer?;
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)1799 // A person who has not confirmed their email address can do
1800 // nothing with git until they do: told so, not asked to sign in.
1801 if viewer.as_ref().is_some_and(g1t_contracts::User::awaits_confirmation) {
1802 let site = request.url()?.origin().ascii_serialization();
1803 return git_http::refuse(Outcome::<()>::fail(
1804 FailureCode::Forbidden,
1805 g1t_contracts::accounts::confirm_email_first(&site),
1806 ));
1807 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1808 // A run credential is checked against its grants, then acts as the
1809 // 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 200ms1810 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 API1811 run_access::Admitted::Go { request, viewer, entry } => (request, viewer, entry),
1812 run_access::Admitted::Refused(response) => return Ok(response),
1813 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1814 let mut after = AfterGit {
1815 audit,
1816 status: 0,
1817 message: None,
1818 push: None,
1819 };
1820 let repo = match self.authorize_git(&git.path, &viewer, git.service, found).await? {
1821 Outcome::Ok(repo) => repo,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1822 refused => {
1823 let response = git_http::refuse(refused)?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1824 after.ended(response.status_code(), None);
1825 after.spawn(env, ctx);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1826 return Ok(response);
1827 }
Rust repos service with shipping; pull requests kept in the model1828 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1829 // A pull request's working copy removed after it closed is made
1830 // again before git uses it (forks.rs).
1831 self.live(&repo).await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1832 timing.mark("access");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1833 // Clones check out the default branch g1t keeps, which can have
1834 // changed since the store made the repository.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1835 let default_branch = repo.fork_of.is_none().then(|| repo.default_branch.clone());
1836 let key = store_key(&repo);
1837 let scope = if write { Scope::Write } else { Scope::Read };
1838 let mut request = request;
1839 let protocol = refs_cache::protocol(request.headers().get("git-protocol")?.as_deref());
1840 // A fetch's POST is read here, to tell an `ls-refs` from a fetch of
1841 // objects; the store would have it read in full anyway.
1842 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 daily1843 // What it asks the store, for the meters (meters.rs).
1844 let call = git_ops::classify(git.service, git.endpoint, get, body.as_deref());
Merge branch 'worktree-agent-a2013627e5ea4ab13'1845 // Answers kept from the usual store may name refs the fallback
1846 // store does not have (fallback.rs): none are used, or kept.
1847 let fallback = self.store.on_fallback(&key);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1848 // An answer that lists refs may have been kept: see refs_cache.rs.
1849 let kept_key = refs_cache::kind(git, get, protocol, body.as_deref())
Merge branch 'worktree-agent-a2013627e5ea4ab13'1850 .filter(|_| !fallback)
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1851 .zip(refs_cache::usable(registry::refs_state(&repo.id), now_ms()))
1852 .map(|(kind, version)| {
1853 refs_cache::Key::new(&repo.id, version, default_branch.as_deref(), protocol, &kind)
1854 });
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1855 // A fresh clone's pack may have been kept too: see pack_cache.rs.
1856 // Under the same refs version, so never across a change to them.
1857 let pack_key = self
1858 .packs
1859 .as_ref()
Merge branch 'worktree-agent-a2013627e5ea4ab13'1860 .filter(|_| !fallback)
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1861 .and_then(|_| {
1862 let encoding = request.headers().get("content-encoding").ok().flatten();
1863 pack_cache::cacheable(git, get, protocol, encoding.as_deref(), body.as_deref())
1864 })
1865 .zip(refs_cache::usable(registry::refs_state(&repo.id), now_ms()))
1866 .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 200ms1867 // A kept answer and the free workspace limits, with a kept
1868 // credential looked up alongside. A kept answer goes back without
1869 // waiting for the credential, which it does not need.
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1870 let ((answer, pack, limited), kept_access) = {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1871 let shared = self.shared.as_deref();
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1872 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 200ms1873 async {
1874 match &kept_key {
1875 Some(kept_key) => refs_cache::get(shared, kept_key).await,
1876 None => None,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1877 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1878 },
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1879 async {
1880 match (&pack_key, self.packs.as_deref()) {
1881 (Some(pack_key), Some(packs)) => pack_cache::get(packs, pack_key).await,
1882 _ => None,
1883 }
1884 },
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1885 self.git_limits(call, git, &repo, env),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1886 ));
1887 let kept_access = std::pin::pin!(self.store.kept_access(&key, scope));
1888 match futures_util::future::select(answer_and_limits, kept_access).await {
1889 futures_util::future::Either::Left((first, kept_access)) => {
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1890 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 200ms1891 (first, if answered { None } else { kept_access.await })
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1892 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1893 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 look1894 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1895 };
1896 timing.mark("kept");
A clone answered from the pack cache is served before the free operation cap: it is not an operation1897 // A kept pack first: it never reaches the store, so it is never an
1898 // operation, and a free workspace past its operation cap still gets
1899 // it. (The other limits are a push's, and a pack is only a fetch.)
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1900 if let Some(kept) = pack {
1901 timing.note("pack", "hit");
1902 let sent = body.as_ref().map_or(0, |body| body.len() as u64);
1903 meters::record(pack_cache::HIT, &key, sent, kept.size);
1904 after.ended(200, None);
1905 after.spawn(env, ctx);
1906 return kept.response();
1907 }
A clone answered from the pack cache is served before the free operation cap: it is not an operation1908 if let Some((response, status, message)) = limited? {
1909 after.ended(status, Some(message.to_owned()));
1910 after.spawn(env, ctx);
1911 return Ok(response);
1912 }
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1913 if pack_key.is_some() {
1914 timing.note("pack", "miss");
1915 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1916 if let (Some((entry, found)), Some(kept_key)) = (answer, &kept_key) {
1917 timing.note("refs", found.as_str());
1918 if found == refs_cache::Found::Shared {
1919 let (kept_key, entry) = (kept_key.clone(), entry.clone());
1920 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 look1921 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1922 // Never reached the store: never an operation.
1923 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 200ms1924 after.ended(200, None);
1925 after.spawn(env, ctx);
1926 return entry.response();
1927 }
1928 if kept_key.is_some() {
1929 timing.note("refs", "miss");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1930 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1931 // The store's credential: one made a moment ago, here or in another
1932 // isolate (see store.rs), or a new one.
1933 let access = match kept_access {
1934 Some((access, from)) => {
1935 timing.note("cred", from.as_str());
1936 access
1937 }
1938 None => {
1939 let access = self.store.mint_access(&key, scope).await?;
1940 timing.mark("mint");
1941 timing.note("cred", "mint");
1942 access
1943 }
1944 };
1945 // Should the store turn a kept credential down, a fetch's first
1946 // request is tried again with a new one; the requests after it then
1947 // have that one too.
1948 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 API1949 // 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 200ms1950 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 merge1951 // Rulesets: what the rules of the branches and tags it changes
1952 // refuse is declined, saying which rule and why (rules.rs).
1953 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 daily1954 // What a push may bring (pack_limits.rs): the repository's size is
1955 // its own and its pull requests' working copies'.
1956 let limits = if write && !get {
1957 git_http::PushLimits {
1958 held: self.held(&repo).await,
1959 repo_limit: self.repo_limit,
1960 large: self.large_pushes,
1961 ..git_http::PushLimits::default()
1962 }
1963 } else {
1964 git_http::PushLimits::default()
1965 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1966 let mut outcome = git_http::forward(
1967 request,
1968 body,
1969 git,
1970 &access,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1971 rules,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1972 default_branch.as_deref(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1973 limits,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1974 scan,
1975 )
1976 .await?;
1977 let turned_down = matches!(
1978 &outcome,
1979 git_http::Push::Forwarded(forwarded) if matches!(forwarded.response.status_code(), 401 | 403)
1980 );
1981 if turned_down {
1982 self.store.forget_access(&key).await;
1983 if let Some(again) = again {
1984 let access = self.store.mint_access(&key, scope).await?;
1985 let nothing = async |_: &[u8]| Ok(None);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1986 outcome = git_http::forward(
1987 again,
1988 None,
1989 git,
1990 &access,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1991 async |_: &[u8], _: bool| Ok(None),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1992 default_branch.as_deref(),
1993 git_http::PushLimits::default(),
1994 nothing,
1995 )
1996 .await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1997 }
1998 }
Agents as a team: lifecycle, merge queue, billing and a new shell1999 let forwarded =
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2000 match outcome {
Agents as a team: lifecycle, merge queue, billing and a new shell2001 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 API2002 git_http::Push::Refused(response) => {
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2003 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 200ms2004 after.spawn(env, ctx);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API2005 return Ok(response);
2006 }
2007 git_http::Push::Blocked(response) => {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2008 after.ended(403, Some("The push adds a secret.".to_owned()));
2009 after.spawn(env, ctx);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API2010 return Ok(response);
2011 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2012 git_http::Push::Declined(response, reason) => {
2013 after.ended(403, Some(format!("The push was declined: {reason}.")));
2014 after.spawn(env, ctx);
2015 return Ok(response);
2016 }
Agents as a team: lifecycle, merge queue, billing and a new shell2017 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2018 if forwarded.from_store {
2019 let received = forwarded
2020 .response
2021 .headers()
2022 .get("content-length")?
2023 .and_then(|length| length.parse().ok())
2024 .unwrap_or(0);
2025 meters::record(call.meter(), &key, forwarded.sent, received);
2026 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2027 timing.mark("store");
2028 let mut response = forwarded.response;
2029 let status = response.status_code();
2030 if write && !get {
2031 // A push: the store has moved its refs once it has answered in
2032 // full, so the answer is read before the change is recorded, and
2033 // only then goes back. Whoever fetches after it sees the push.
2034 let headers = response.headers().clone();
2035 headers.delete("content-length")?;
2036 let report = response.bytes().await?;
2037 self.refs_moved(&repo.id).await;
2038 timing.mark("refs");
2039 response = Response::from_bytes(report)?.with_headers(headers).with_status(status);
2040 } else if let (Some(kept_key), 200) = (&kept_key, status) {
2041 // A miss: this answer is kept for the next to ask.
2042 let headers = response.headers().clone();
2043 headers.delete("content-length")?;
2044 let body = response.bytes().await?;
2045 if let Some(content_type) = headers.get("content-type")? {
2046 let entry = refs_cache::Entry { content_type, body: body.clone() };
2047 if entry.keepable() {
2048 let shared = self.shared.clone();
2049 let kept_key = kept_key.clone();
2050 ctx.wait_until(async move { refs_cache::keep(shared.as_deref(), &kept_key, &entry).await });
2051 }
2052 }
2053 response = Response::from_bytes(body)?.with_headers(headers).with_status(status);
Merge branch 'worktree-agent-a1b995daa94e4e1b7'2054 } else if let (Some(pack_key), Some(packs), true) = (&pack_key, &self.packs, forwarded.from_store) {
2055 // A fresh clone the bucket did not have: counted, and its pack
2056 // kept as it streams to git, when it is a whole one.
2057 meters::record(pack_cache::MISS, &key, forwarded.sent, 0);
2058 if status == 200 {
2059 let store_key = key.clone();
2060 let measured = Box::new(move |bytes: u64| meters::record_bytes(pack_cache::MISS, &store_key, 0, bytes));
2061 let (teed, filling) = pack_cache::tee(response, packs.clone(), pack_key, measured)?;
2062 response = teed;
2063 if let Some(filling) = filling {
2064 let pack_key = pack_key.clone();
2065 ctx.wait_until(async move {
2066 let filled = filling.await;
2067 if !matches!(filled, pack_cache::Filled::Kept { .. } | pack_cache::Filled::Abandoned) {
2068 worker::console_warn!("pack {} not kept: {filled:?}", pack_key.as_str());
2069 }
2070 });
2071 }
2072 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2073 }
2074 after.ended(status, None);
2075 if status == 200 && (forwarded.pack_bytes > 0 || !forwarded.pushed.is_empty()) {
2076 after.push = Some(PushDone {
2077 repo,
2078 pushed: forwarded.pushed,
2079 pack_bytes: forwarded.pack_bytes,
Merge branch 'worktree-agent-a3abfcce648e87dca'2080 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 200ms2081 actor: viewer.map(|user: User| user.id),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2082 unscanned: forwarded.unscanned,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2083 });
2084 }
2085 after.spawn(env, ctx);
2086 Ok(response)
2087 }
2088
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2089 /// The answer for a request a free workspace's limits stop, or a push
2090 /// to a full repository, with its status and reason for the audit log;
2091 /// `None` to go on.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2092 ///
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2093 /// A clone, fetch or push is a git operation, which the git store
2094 /// charges g1t for: counted for billing once the answer has gone back
2095 /// (meters.rs), and a free workspace far past its share is slowed down
2096 /// rather than charged (see git_ops.rs). Whether it is past it is
2097 /// decided from counts this isolate already holds: the database is not
2098 /// asked on the way. A free workspace is never charged for private
2099 /// storage: once its private repositories hold the free amount, pushes
2100 /// to them stop, checked when a push begins so that git shows the
2101 /// 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 200ms2102 async fn git_limits(
2103 &self,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2104 call: git_ops::GitCall,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2105 git: &git_http::GitRequest,
2106 repo: &Repo,
2107 env: &Env,
2108 ) -> Result<Option<(Response, u16, &'static str)>> {
2109 let namespace = git.path.namespace.to_lowercase();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2110 if meters::mapping_now().billable(call.meter()) > 0.0 {
2111 let now = now_ms();
2112 let hour = git_ops::hour_key(&rfc3339(now));
2113 let limits = git_ops::Limits::from_env(env);
2114 if let Some((month, hour_ops)) = git_ops::standing(&namespace, &hour, now)
2115 && git_ops::slow_down(month + 1, hour_ops + 1, limits.free_cap, limits.hourly)
2116 && git_ops::is_free_kept(env.service("BILLING").ok().as_ref(), &namespace).await
2117 {
2118 return Ok(Some((
2119 git_ops::too_many(&namespace, limits.free_cap, limits.hourly)?,
2120 429,
2121 "Too many git operations this hour.",
2122 )));
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2123 }
2124 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2125 if git.service == GitService::ReceivePack && git.endpoint == "info/refs" {
2126 let held = self.held(repo).await;
2127 if held >= self.repo_limit {
2128 let message = format!(
2129 "{}/{} 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",
2130 repo.namespace,
2131 repo.name,
2132 pack_limits::megabytes(held)
2133 );
2134 return Ok(Some((Response::error(message, 403)?, 403, "The repository is full.")));
2135 }
2136 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2137 if git.service == GitService::ReceivePack && git.endpoint == "info/refs" && repo.is_private {
2138 let free = git_ops::free_private_bytes(env);
2139 let held = self.registry.private_bytes(&namespace).await.unwrap_or(0);
2140 if git_ops::storage_full(held, free)
2141 && git_ops::is_free(env.service("BILLING").ok().as_ref(), &namespace).await
2142 {
2143 return Ok(Some((
2144 git_ops::storage_full_response(&namespace, held, free)?,
2145 403,
2146 "Free private storage is full.",
2147 )));
2148 }
2149 }
2150 Ok(None)
2151 }
Rust repos service with shipping; pull requests kept in the model2152
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2153 /// What a repository and its pull requests' working copies hold, as
2154 /// g1t counts it: read for a push's first request, kept a minute for
2155 /// the rest of it.
2156 async fn held(&self, repo: &Repo) -> u64 {
2157 let root = repo.fork_of.clone().unwrap_or_else(|| repo.id.clone());
2158 let now = now_ms();
2159 if let Some(held) = HELD.with(|held| held.borrow().get(&root, now)) {
2160 return held;
2161 }
2162 let held = self.registry.stored_bytes(&root).await.unwrap_or(0).max(0) as u64;
2163 HELD.with(|kept| kept.borrow_mut().put(root, held, now));
2164 held
2165 }
2166
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2167 /// What a push changed, recorded once git has its answer.
2168 async fn record_push(&self, push: PushDone) -> Result<()> {
2169 let PushDone {
2170 repo,
2171 pushed,
2172 pack_bytes,
2173 actor,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2174 unscanned,
Merge branch 'worktree-agent-a3abfcce648e87dca'2175 caused_by_job,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2176 } = push;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2177 // What the push stored, for billing's storage meter. A failure only
2178 // leaves the count short.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2179 if pack_bytes > 0
2180 && 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 put2181 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2182 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 put2183 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2184 if pushed.is_empty() {
2185 return Ok(());
2186 }
Rust repos service with shipping; pull requests kept in the model2187 // Artifacts' own push notifications are per repository, which does
Events service in Rust, with RFC 3339 times and accurate push events2188 // not fit a repo per pull request, so the front end reports pushes
2189 // 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 200ms2190 let stored = self.store.open(&store_key(&repo)).await?;
2191 for pushed in &pushed {
2192 // The store can refuse one ref and accept another, so each
2193 // branch is checked against where it actually is. A tag the
2194 // store cannot read back is taken as pushed.
2195 let moved = match pushed.branch() {
2196 Some(branch) => stored
2197 .log(branch, 1)
2198 .await?
2199 .first()
2200 .is_some_and(|commit| commit.hash == pushed.after),
2201 None => stored.log(&pushed.git_ref, 1).await.map_or(true, |head| {
2202 head.first().is_none_or(|commit| commit.hash == pushed.after)
2203 }),
2204 };
2205 if moved {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2206 self.publish_git_push(
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2207 &repo,
2208 &pushed.git_ref,
2209 pushed.before.as_deref(),
2210 &pushed.after,
2211 actor.clone(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2212 unscanned,
Merge branch 'worktree-agent-a3abfcce648e87dca'2213 caused_by_job.clone(),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2214 )
2215 .await?;
2216 }
2217 }
2218 Ok(())
2219 }
2220}
2221
2222/// A push the store accepted, to be recorded once git has its answer.
2223struct PushDone {
2224 repo: Repo,
2225 pushed: Vec<git_http::Pushed>,
2226 pack_bytes: u64,
2227 actor: Option<String>,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2228 /// Too large to scan for secrets before it was stored.
2229 unscanned: bool,
Merge branch 'worktree-agent-a3abfcce648e87dca'2230 /// The run whose job's token pushed, if one did: its push starts no
2231 /// workflows.
2232 caused_by_job: Option<String>,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2233}
2234
2235/// What a git request leaves for after its answer: its audit entry, with
2236/// how the request ended, and what a push changed.
2237struct AfterGit {
2238 audit: Option<Box<g1t_contracts::audit::NewAuditEntry>>,
2239 status: u16,
2240 message: Option<String>,
2241 push: Option<PushDone>,
2242}
2243
2244impl AfterGit {
2245 fn ended(&mut self, status: u16, message: Option<String>) {
2246 self.status = status;
2247 self.message = message;
2248 }
2249
2250 /// Does the work once the response is on its way. A failure is logged:
2251 /// git has already been told how its request went.
2252 fn spawn(self, env: &Env, ctx: &Context) {
2253 if self.audit.is_none() && self.push.is_none() {
2254 return;
2255 }
2256 let env = env.clone();
2257 ctx.wait_until(async move {
2258 let repos = match service(&env) {
2259 Ok(repos) => repos,
2260 Err(error) => {
2261 worker::console_error!("git request not recorded: {error}");
2262 return;
Events service in Rust, with RFC 3339 times and accurate push events2263 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2264 };
2265 repos.finish_git(self.audit, self.status, self.message).await;
2266 if let Some(push) = self.push
2267 && let Err(error) = repos.record_push(push).await
2268 {
2269 worker::console_error!("push not recorded: {error}");
Rust repos service with shipping; pull requests kept in the model2270 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2271 });
Rust repos service with shipping; pull requests kept in the model2272 }
2273}
2274
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2275fn service(env: &Env) -> Result<Repos<ArtifactsStore>> {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2276 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 look2277 Ok(Repos {
Rust repos service with shipping; pull requests kept in the model2278 registry: Registry { db: env.d1("DB")? },
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2279 store: ArtifactsStore::new(env, shared.clone())?,
2280 shared,
Merge branch 'worktree-agent-aaf03bdceac799c89'2281 packs: pack_cache::Packs::from_env(env).map(Rc::new),
Events service in Rust, with RFC 3339 times and accurate push events2282 events: env.service("EVENTS")?,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API2283 security: env.service("SECURITY").ok(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2284 billing: env.service("BILLING").ok(),
2285 identity: env.service("IDENTITY").ok(),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2286 work: env.service("WORK").ok(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2287 free_private_bytes: git_ops::free_private_bytes(env),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2288 fork_days: forks::retention_days(env),
2289 repo_limit: env
2290 .var("REPO_STORAGE_LIMIT_BYTES")
2291 .ok()
2292 .and_then(|value| value.to_string().parse().ok())
2293 .unwrap_or(pack_limits::DEFAULT_REPO_LIMIT_BYTES),
2294 large_pushes: git_http::LargePushes::from_var(env.var("LARGE_PUSHES").ok().map(|value| value.to_string()).as_deref()),
2295 placement: shards::Placement::from_vars(
2296 env.var("ARTIFACTS_NEW_REPOS").ok().map(|value| value.to_string()).as_deref(),
2297 env.var("ARTIFACTS_EU_NAMESPACE").ok().map(|value| value.to_string()).as_deref(),
2298 ),
Merge branch 'worktree-agent-a2013627e5ea4ab13'2299 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 look2300 })
2301}
2302
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2303/// Writes what this isolate metered once the answer has gone back, every
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2304/// few seconds at most: now, or once it is due, waiting in this request's
2305/// `wait_until` so nothing counted is left for a request that may never
2306/// come (meters.rs).
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2307fn flush_later(env: &Env, ctx: &Context) {
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2308 let Some(wait) = meters::plan_flush() else {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2309 return;
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2310 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2311 if let Ok(db) = env.d1("DB") {
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2312 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 daily2313 }
2314}
2315
Merge branch 'worktree-agent-ac5b181a013e54348'2316/// `/backups/<job id>/parts/<number>`: the job and the part's number.
2317fn backup_part_path(path: &str) -> Option<(String, u16)> {
2318 let rest = path.strip_prefix("/backups/")?;
2319 let (job, number) = rest.split_once("/parts/")?;
2320 let number = number.parse::<u16>().ok()?;
2321 (!job.is_empty() && !job.contains('/')).then(|| (job.to_owned(), number))
2322}
2323
2324fn backups_off<T>() -> Outcome<T> {
2325 Outcome::fail(FailureCode::Conflict, "Backups are off on this installation: it has no storage for them.")
2326}
2327
2328/// One part of a backup's bundle, with the job's token in its header.
2329async fn backup_part(request: &mut Request, env: &Env, repos: &Repos<ArtifactsStore>, job_id: String, number: u16) -> Result<Response> {
2330 let Some(blobs) = backups::storage(env) else {
2331 return reply(&backups_off::<()>());
2332 };
2333 let token = request.headers().get(g1t_contracts::backups::TOKEN_HEADER)?.unwrap_or_default();
2334 let bytes = request.bytes().await?;
2335 let job = g1t_contracts::backups::BackupJobArgs { job_id, token };
2336 reply(&backups::part(&repos.registry.db, &blobs, &job, number, bytes).await?)
2337}
2338
2339#[cfg(test)]
2340mod backup_path_tests {
2341 use super::backup_part_path;
2342
2343 #[test]
2344 fn a_part_is_named_by_its_job_and_number() {
2345 assert_eq!(backup_part_path("/backups/bkp_1/parts/3"), Some(("bkp_1".to_owned(), 3)));
2346 assert_eq!(backup_part_path("/backups/bkp_1/parts/x"), None);
2347 assert_eq!(backup_part_path("/backups//parts/1"), None);
2348 assert_eq!(backup_part_path("/acme/rocket.git/info/refs"), None);
2349 }
2350}
2351
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2352/// Read methods whose answer is an `Outcome`: when the git store is busy,
2353/// 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 252354const 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 daily2355
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2356#[event(fetch)]
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2357async fn fetch(mut request: Request, env: Env, ctx: Context) -> Result<Response> {
Fast pages, required checks on the branch, self-hosted runners, honest incidents2358 let mut repos = service(&env)?;
Merge branch 'worktree-agent-ac5b181a013e54348'2359 // A part of a backup's bundle, as the API passes it on from the
2360 // sandbox: bytes, not JSON (backups.rs).
2361 if request.method() == Method::Put
2362 && let Some((job_id, number)) = backup_part_path(&request.path())
2363 {
2364 let answered = backup_part(&mut request, &env, &repos, job_id, number).await;
2365 flush_later(&env, &ctx);
2366 return answered;
2367 }
Rust repos service with shipping; pull requests kept in the model2368 let Some(method) = rpc_method(&request) else {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2369 let answered = repos.git_http(request, &env, &ctx).await;
2370 flush_later(&env, &ctx);
2371 return answered;
Rust repos service with shipping; pull requests kept in the model2372 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents2373 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
2374 // Git over HTTPS above always reads the primary.
2375 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
2376 repos.registry.db = db;
Rust repos service with shipping; pull requests kept in the model2377 let body: serde_json::Value = request.json().await?;
2378
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2379 let answered = async { match method.as_str() {
Rust repos service with shipping; pull requests kept in the model2380 "get" => reply(&repos.get(args(body)?).await?),
2381 "get_by_id" => reply(&repos.get_by_id(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2382 "readable" => {
2383 let a: ReadableArgs = args(body)?;
2384 reply(&repos.registry.readable(&a.ids, &a.viewer).await?)
2385 }
2386 "public_namespaces" => {
2387 let a: PublicNamespacesArgs = args(body)?;
2388 reply(&repos.registry.public_namespaces(&a.owner_id).await?)
2389 }
Automations: rules in .g1t/automations that act when something happens2390 "path_by_id" => {
2391 let a: PathByIdArgs = args(body)?;
2392 reply(
2393 &repos
2394 .registry
2395 .by_id(&a.id)
2396 .await?
2397 .filter(|repo| repo.fork_of.is_none())
2398 .map(|repo| RepoPath {
2399 namespace: repo.namespace,
2400 name: repo.name,
2401 }),
2402 )
2403 }
Rust repos service with shipping; pull requests kept in the model2404 "list" => {
2405 let a: ListArgs = args(body)?;
2406 reply(
2407 &repos
2408 .registry
Workspaces own repositories2409 .list(
2410 &a.viewer,
2411 a.query.as_deref(),
2412 a.namespace.as_deref(),
2413 a.member_only,
2414 )
Rust repos service with shipping; pull requests kept in the model2415 .await?,
2416 )
2417 }
2418 "create" => reply(&repos.create(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2419 // Services only: a GitHub mirror catching up, or pushing out.
2420 "mirror" => reply(&repos.mirror(args(body)?).await?),
2421 "transfer" => reply(&repos.transfer(args(body)?).await?),
2422 // A repository's lifecycle: see lifecycle.rs.
2423 "delete" => reply(&repos.delete(args(body)?).await?),
2424 "deleted" => reply(&repos.deleted(args(body)?).await?),
2425 "restore" => reply(&repos.restore(args(body)?).await?),
2426 "purge" => reply(&repos.purge(args(body)?).await?),
2427 "purge_due" => reply(&repos.purge_due(args(body)?).await?),
2428 "rename" => reply(&repos.rename(args(body)?).await?),
2429 "archive" => reply(&repos.archive(args(body)?).await?),
2430 "set_visibility" => reply(&repos.set_visibility(args(body)?).await?),
2431 "set_default_branch" => reply(&repos.set_default_branch(args(body)?).await?),
2432 "rename_branch" => reply(&repos.rename_branch(args(body)?).await?),
2433 "resolve_branch" => reply(&repos.resolve_branch(args(body)?).await?),
2434 "status_by_id" => reply(&repos.status_by_id(args(body)?).await?),
2435 "resolve_path" => {
2436 let a: ResolvePathArgs = args(body)?;
2437 reply(&repos.registry.resolve_moved(&a.path).await?)
2438 }
2439 "namespace_count" => {
2440 let a: NamespaceCountArgs = args(body)?;
2441 reply(&repos.registry.count_in(&a.namespace).await?)
2442 }
Agents as a team: lifecycle, merge queue, billing and a new shell2443 "update" => reply(&repos.update(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model2444 "tree" => reply(&repos.tree(args(body)?).await?),
2445 "blob" => reply(&repos.blob(args(body)?).await?),
2446 "log" => reply(&repos.log(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell2447 "blame" => reply(&repos.blame(args(body)?).await?),
Issues and pull requests replace intents and attempts2448 "fork_for_pull" => reply(&repos.fork_for_pull(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model2449 "git_access" => reply(&repos.git_access(args(body)?).await?),
Pull requests from branches2450 "branches" => reply(&repos.branches(args(body)?).await?),
Branches and Tags pages, each file's last commit, and the branch menu on files2451 "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 252452 "branch_drift" => reply(&repos.branch_drift(args(body)?).await?),
Branches and Tags pages, each file's last commit, and the branch menu on files2453 "tags" => reply(&repos.tags(args(body)?).await?),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb972454 // The About: what the Files page shows beside the files (about.rs).
2455 // What is kept behind the head is worked out again after the answer.
2456 "about" => {
2457 let (answer, refresh) = repos.about(args(body)?).await?;
2458 about::refresh_later(&env, &ctx, refresh);
2459 reply(&answer)
2460 }
2461 "languages" => {
2462 let (answer, refresh) = repos.languages(args(body)?).await?;
2463 about::refresh_later(&env, &ctx, refresh);
2464 reply(&answer)
2465 }
2466 "contributors" => {
2467 let (answer, refresh) = repos.contributors(args(body)?).await?;
2468 about::refresh_later(&env, &ctx, refresh);
2469 reply(&answer)
2470 }
2471 "license" => {
2472 let (answer, refresh) = repos.license(args(body)?).await?;
2473 about::refresh_later(&env, &ctx, refresh);
2474 reply(&answer)
2475 }
2476 "stars" => reply(&repos.stars(args(body)?).await?),
2477 "star" => reply(&repos.star(args(body)?).await?),
2478 "stargazers" => reply(&repos.stargazers(args(body)?).await?),
2479 "starred" => reply(&repos.starred(args(body)?).await?),
2480 "releases" => reply(&repos.releases(args(body)?).await?),
2481 "release" => reply(&repos.release(args(body)?).await?),
2482 "create_release" => reply(&repos.create_release(args(body)?).await?),
2483 "update_release" => reply(&repos.update_release(args(body)?).await?),
2484 "delete_release" => reply(&repos.delete_release(args(body)?).await?),
Pull requests from branches2485 "head" => reply(&repos.head(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell2486 "behind" => reply(&repos.behind(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2487 "divergence" => reply(&repos.divergence(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model2488 "land" => reply(&repos.land(args(body)?).await?),
Catching up with main takes seconds when the two sides touched different files2489 "update_pull_branch" => reply(&repos.update_pull_branch(args(body)?).await?),
Merge queue: tested states are deleted once their entry leaves2490 "delete_branch" => reply(&repos.delete_branch(args(body)?).await?),
Fast pages, required checks on the branch, self-hosted runners, honest incidents2491 "commit_file" => reply(&repos.commit_file(args(body)?).await?),
Diffs on attempts; hosted agent presented as the g1t agent2492 "compare" => reply(&repos.compare(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2493 // Services only: a pull request's commits, as rules look at them (rules.rs).
2494 "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 API2495 "scan_history" => reply(&repos.scan_history(args(body)?).await?),
2496 "find_lockfiles" => reply(&repos.find_lockfiles(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2497 "match_pattern" => reply(&repos.match_pattern(args(body)?).await?),
2498 "check_secret" => reply(&repos.check_secret(args(body)?).await?),
Search across all of g1t, Explore, and a command palette2499 "list_files" => reply(&repos.list_files(args(body)?).await?),
2500 "changed_files" => reply(&repos.changed_files(args(body)?).await?),
2501 "read_blobs" => reply(&repos.read_blobs(args(body)?).await?),
Composer from the workspace's own repositories, and go get from g1t.sh2502 // Services only: what the Composer registry builds packages from.
2503 "refs" => reply(&repos.refs_of(args(body)?).await?),
2504 "raw_file" => reply(&repos.raw_file(args(body)?).await?),
2505 "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 put2506 "visibility" => {
2507 let a: g1t_contracts::repos::VisibilityArgs = args(body)?;
2508 reply(&repos.registry.visibility(&a.paths).await?)
2509 }
2510 "storage" => reply(&repos.registry.storage().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2511 "git_operations" => {
2512 let a: GitOperationsArgs = args(body)?;
2513 reply(&git_ops::totals(&repos.registry.db, &a.month, a.since.as_deref(), a.namespace.as_deref().map(str::to_lowercase).as_deref()).await?)
2514 }
Merge main (membership, two-factor, GitHub repo roles) into tokens2515 // Identity, once: who created each repository (members.rs there).
2516 "repo_creators" => {
2517 let a: AllIdsArgs = args(body)?;
2518 let limit = a.limit.clamp(1, 500);
2519 let repos = repos.registry.creators_after(a.after.as_deref(), limit).await?;
2520 let next = (repos.len() == limit as usize).then(|| repos.last().map(|repo| repo.id.clone())).flatten();
2521 reply(&CreatorPage { repos, next })
2522 }
Search across all of g1t, Explore, and a command palette2523 "all_ids" => {
2524 let a: AllIdsArgs = args(body)?;
2525 let limit = a.limit.clamp(1, 500);
2526 let ids = repos.registry.ids_after(a.after.as_deref(), limit).await?;
2527 let next = (ids.len() == limit as usize).then(|| ids.last().cloned()).flatten();
2528 reply(&IdPage { ids, next })
2529 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2530 // The raw meters of the git store, for reconciling with Cloudflare
2531 // (meters.rs, scripts/ops/artifacts-usage.mjs).
2532 "artifacts_usage" => {
2533 let a: meters::UsageArgs = args(body)?;
2534 reply(&meters::usage(&repos.registry.db, &a).await?)
2535 }
2536 "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)2537 // Billing: the workspace each pull request's working copy is counted
2538 // for, so Cloudflare's own count of `pulls--<id>` shares out too.
2539 "pull_owners" => {
2540 #[derive(serde::Deserialize)]
2541 struct PullOwnersArgs {
2542 pulls: Vec<String>,
2543 }
2544 let a: PullOwnersArgs = args(body)?;
2545 let pulls: Vec<String> = a.pulls.into_iter().take(500).collect();
2546 reply(&serde_json::json!({ "owners": meters::pull_owners(&repos.registry.db, &pulls).await? }))
2547 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2548 // Services only: which meters are operations, changed without a deploy.
2549 "set_operation_mapping" => {
2550 let row: meters::MappingRow = args(body)?;
2551 meters::set_mapping(&repos.registry.db, &row, &rfc3339(now_ms())).await?;
2552 reply(&meters::read_mapping(&repos.registry.db).await?)
2553 }
Merge branch 'worktree-agent-ac5b181a013e54348'2554 // Backups (backups.rs): the runner's sweep claims queued ones, and
2555 // each sandbox, through the API, asks for its job and says how it went.
2556 "claim_backups" => {
2557 let a: g1t_contracts::backups::ClaimBackupsArgs = args(body)?;
2558 let blobs = backups::storage(&env);
2559 reply(&backups::claim(&repos.registry.db, blobs.as_ref(), &a, now_ms()).await?)
2560 }
2561 "backup_spec" => match backups::storage(&env) {
2562 Some(blobs) => {
2563 let a: g1t_contracts::backups::BackupJobArgs = args(body)?;
2564 let every = backups::Settings::from_env(&env).full_every;
2565 reply(&backups::spec(&repos.registry, &blobs, &repos.store, &a, every, now_ms()).await?)
2566 }
2567 None => reply(&backups_off::<bool>()),
2568 },
2569 "backup_complete" => match backups::storage(&env) {
2570 Some(blobs) => reply(&backups::complete(&repos.registry, &blobs, &args(body)?, now_ms()).await?),
2571 None => reply(&backups_off::<bool>()),
2572 },
2573 "backup_fail" => match backups::storage(&env) {
2574 Some(blobs) => reply(&backups::fail(&repos.registry.db, &blobs, &args(body)?).await?),
2575 None => reply(&backups_off::<bool>()),
2576 },
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2577 // How the git store has been answering, for the status page.
2578 "store_health" => {
2579 let a: meters::HealthArgs = args(body)?;
2580 reply(&meters::health(&repos.registry.db, &a).await?)
2581 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'2582 // Where repositories may be kept, for a workspace's settings.
2583 "storage_options" => reply(&repos.storage_options()),
2584 // Services and operators only: how each namespace stands, and
2585 // moving a repository between them (namespaces.rs, moves.rs).
2586 "namespaces" => reply(&repos.standings().await?),
2587 "move_repository" => reply(&repos.move_repository(args(body)?).await?),
2588 "repository_moves" => {
2589 let a: moves::ListMovesArgs = args(body)?;
2590 reply(&repos.registry.moves(a.limit.unwrap_or(50)).await?)
2591 }
Rust repos service with shipping; pull requests kept in the model2592 _ => Response::error("Unknown method", 404),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2593 } }
2594 .await;
2595 // The git store is busy: said in words, with when to try again.
2596 let answered = match answered {
2597 Err(error) => match resilience::busy(&error.to_string()) {
2598 Some(busy) if OUTCOME_READS.contains(&method.as_str()) => {
2599 reply(&Outcome::<()>::fail(FailureCode::Conflict, busy.message().trim()))
2600 }
2601 Some(busy) => {
2602 let response = Response::error(busy.message(), 503)?;
2603 response.headers().set("retry-after", &busy.retry_after.to_string())?;
2604 Ok(response)
2605 }
2606 None => Err(error),
2607 },
2608 answered => answered,
Fast pages, required checks on the branch, self-hosted runners, honest incidents2609 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2610 flush_later(&env, &ctx);
Fast pages, required checks on the branch, self-hosted runners, honest incidents2611 served.finish(answered)
Rust repos service with shipping; pull requests kept in the model2612}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2613
Merge branch 'worktree-agent-ac5b181a013e54348'2614/// The nightly cron in wrangler.jsonc: tonight's backups are queued.
2615const BACKUP_CRON: &str = "53 2 * * *";
2616
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2617/// The hourly sweep: deleted repositories whose time to be restored has
Merge branch 'worktree-agent-ac5b181a013e54348'2618/// passed are purged. See lifecycle.rs. And, at [`BACKUP_CRON`], the
2619/// 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 look2620#[event(scheduled)]
Merge branch 'worktree-agent-ac5b181a013e54348'2621async fn scheduled(event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2622 let repos = match service(&env) {
2623 Ok(repos) => repos,
2624 Err(error) => {
2625 worker::console_error!("repos: the sweep could not start: {error}");
2626 return;
2627 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2628 };
Merge branch 'worktree-agent-ac5b181a013e54348'2629 if event.cron() == BACKUP_CRON {
2630 let Some(blobs) = backups::storage(&env) else { return };
2631 match backups::nightly(&repos.registry.db, &blobs, backups::Settings::from_env(&env), now_ms()).await {
2632 Ok(night) => worker::console_log!("repos: queued {} backups, removed {} of purged repositories", night.queued, night.pruned),
2633 Err(error) => worker::console_error!("repos: backups could not be queued: {error}"),
2634 }
2635 return;
2636 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2637 match repos.purge_due(PurgeDueArgs::default()).await {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2638 Ok(0) => {}
2639 Ok(count) => worker::console_log!("repos: purged {count} deleted repositories"),
2640 Err(error) => worker::console_error!("repos: the purge sweep failed: {error}"),
2641 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2642 // Pull requests' working copies whose time has come (forks.rs).
2643 match repos.retire_due().await {
2644 Ok(0) => {}
2645 Ok(count) => worker::console_log!("repos: removed {count} pull request working copies"),
2646 Err(error) => worker::console_error!("repos: the working copy sweep failed: {error}"),
2647 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'2648 // Repositories moving between namespaces, and old copies (moves.rs).
2649 match repos.run_moves().await {
2650 Ok(0) => {}
2651 Ok(count) => worker::console_log!("repos: moved {count} repositories between namespaces"),
2652 Err(error) => worker::console_error!("repos: the move sweep failed: {error}"),
2653 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2654 meters::flush(&repos.registry.db).await;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2655}
2656
2657/// Events from the bus. A workspace's rename: its repositories move to the
2658/// workspace's current slug, asked of identity by id, so a repeated or late
2659/// 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 member2660/// were. A workspace's deletion: its repositories are deleted with it,
2661/// restored with it, or purged with it.
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2662#[event(queue)]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2663async fn queue(batch: MessageBatch<Event>, env: Env, ctx: Context) -> Result<()> {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2664 let registry = Registry { db: env.d1("DB")? };
2665 let identity = env.service("IDENTITY")?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2666 let handled = handle_events(&batch, &env, &registry, &identity).await;
2667 flush_later(&env, &ctx);
2668 handled
2669}
2670
2671async fn handle_events(batch: &MessageBatch<Event>, env: &Env, registry: &Registry, identity: &Fetcher) -> Result<()> {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2672 for message in batch.messages()? {
2673 let event = message.body();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2674 // A pull request merged, closed or reopened: its working copy is
2675 // kept or let go (forks.rs).
2676 if let Some(change) = forks::pull_change(&event.kind) {
2677 let Some(pull_id) = forks::pull_id_of(&event.data) else {
2678 worker::console_error!("{} {} names no pull request", event.kind, event.id);
2679 continue;
2680 };
2681 let repos = service(env)?;
2682 match change {
2683 forks::PullChange::Settled => repos.pull_settled(&pull_id).await?,
2684 forks::PullChange::Reopened => repos.pull_reopened(&pull_id).await?,
2685 }
2686 continue;
2687 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member2688 // A workspace deleted, restored or purged: its repositories go with
2689 // it, come back with it, or are purged with it (lifecycle.rs).
2690 if event.kind == "workspace.deleting" {
2691 match serde_json::from_value::<WorkspaceDeleting>(event.data.clone()) {
2692 Ok(deleting) => service(env)?.delete_with_workspace(&deleting, &protected_workspaces(env)).await?,
2693 Err(_) => worker::console_error!("workspace.deleting {} could not be read", event.id),
2694 }
2695 continue;
2696 }
2697 if event.kind == "workspace.restored" {
2698 match serde_json::from_value::<WorkspaceRestored>(event.data.clone()) {
2699 Ok(restored) => service(env)?.restore_with_workspace(&restored).await?,
2700 Err(_) => worker::console_error!("workspace.restored {} could not be read", event.id),
2701 }
2702 continue;
2703 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2704 if event.kind == "workspace.deleted" {
2705 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 member2706 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 look2707 Err(_) => worker::console_error!("workspace.deleted {} could not be read", event.id),
2708 }
2709 continue;
2710 }
Merge leftovers: plan activity filtered in the query, pushes counted by account, ghost during the deletion window, profile time zones (identity 0039)2711 // An account deleted or restored: kept contributors that name it
2712 // (or ghost, for a restore) are worked out again, so it shows as
2713 // ghost for its 30 days, and as itself again if restored.
2714 if let Some(needle) = stats::shown_differently(&event.kind, &event.data) {
2715 let db = env.d1("DB")?;
2716 if let Err(error) = stats::rework_naming(&db, &needle).await {
2717 worker::console_error!("{} {}: contributors not marked to be counted again: {error}", event.kind, event.id);
2718 }
2719 continue;
2720 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2721 if event.kind != "workspace.renamed" {
2722 continue;
2723 }
2724 let Ok(renamed) = serde_json::from_value::<WorkspaceRenamed>(event.data.clone()) else {
2725 worker::console_error!("workspace.renamed {} could not be read", event.id);
2726 continue;
2727 };
2728 let names: HashMap<String, String> = g1t_kit::call(
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2729 identity,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2730 "usernames",
2731 &g1t_contracts::identity::UsernamesArgs {
2732 ids: vec![renamed.workspace_id.clone()],
2733 },
2734 )
2735 .await?;
2736 let current = names
2737 .get(&renamed.workspace_id)
2738 .cloned()
2739 .unwrap_or_else(|| renamed.to.clone());
2740 let left = registry
2741 .rename_namespace(&renamed.stale_slugs(&current), &current)
2742 .await?;
2743 if left > 0 {
2744 worker::console_error!(
2745 "{left} repositories stayed under {} or {}: {current} already has repositories of the same names",
2746 renamed.from,
2747 renamed.to
2748 );
2749 }
2750 }
2751 Ok(())
2752}
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2753
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member2754/// The workspaces whose repositories never go with a deletion, whatever is
2755/// published: `PROTECTED_WORKSPACES` if set here, and Flagon's always.
2756fn protected_workspaces(env: &Env) -> Vec<String> {
2757 let configured = env.var("PROTECTED_WORKSPACES").ok().map(|v| v.to_string());
2758 g1t_contracts::identity::protected_names(configured.as_deref())
2759}
2760
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca2761/// What a token's own limits say about git on the repository `repo`
2762/// (`owner/name`), before anyone's role is asked: why it is refused, or
2763/// `None`. `source` is the repository a pull request's working copy at
2764/// `repo` belongs to, which a workflow job's token reaches too. `public`
2765/// is whether anyone may read it, and `exists` whether there is one.
2766///
2767/// A job's token and a deploy key reach their own repository only. Its
2768/// scopes decide the rest: `code:read` to read a private repository,
2769/// `code:write` to push, which a read-only deploy key never has. A deploy
2770/// key never makes a repository by pushing to an empty address.
2771pub(crate) fn git_token_refusal(
2772 access: &g1t_contracts::scopes::TokenAccess,
2773 repo: &str,
2774 source: Option<&str>,
2775 write: bool,
2776 public: bool,
2777 exists: bool,
2778) -> Option<String> {
2779 if let Some(refused) = g1t_contracts::scopes::decide_repo(access, repo)
2780 && !source.is_some_and(|source| access.reaches(source))
2781 {
2782 return Some(refused.reason.unwrap_or_default());
2783 }
2784 let decision = g1t_contracts::scopes::decide_git(access, write, public);
2785 if !decision.allowed {
2786 return Some(decision.reason.unwrap_or_default());
2787 }
2788 if access.deploy_key.is_some() && !exists {
2789 return Some(format!("This deploy key is for {repo}, which is not there any more."));
2790 }
2791 None
2792}
2793
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2794/// The repository a push to a path that does not exist yet creates: private,
2795/// so nothing pushed by mistake is published. An owner makes it public on
2796/// purpose (`POST /repos/{owner}/{repo}/visibility`).
2797fn push_to_create(owner: &User, path: &RepoPath) -> CreateArgs {
2798 CreateArgs {
2799 owner: owner.clone(),
2800 namespace: path.namespace.clone(),
2801 name: path.name.clone(),
2802 description: None,
2803 is_private: true,
2804 import_url: None,
2805 import_token: None,
2806 }
2807}
2808
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches2809/// Whether `delete_branch` may remove `branch`: one of g1t's own
2810/// (`g1t-…`), or one whose tip the caller names, such as a dependency
2811/// update's branch after its pull request closed.
2812fn deletable_branch(branch: &str, head: Option<&str>) -> bool {
2813 !branch.is_empty() && (branch.starts_with(G1T_BRANCH_PREFIX) || head.is_some_and(|head| !head.is_empty()))
2814}
2815
2816#[cfg(test)]
2817mod delete_branch_tests {
2818 use super::deletable_branch;
2819
2820 #[test]
2821 fn only_g1t_branches_or_a_named_tip_are_deleted() {
2822 assert!(deletable_branch("g1t-queue-12", None));
2823 assert!(!deletable_branch("g1t/security/sharp-0.35.5", None));
2824 assert!(deletable_branch("g1t/security/sharp-0.35.5", Some("abc123")));
2825 assert!(!deletable_branch("feature", Some("")));
2826 assert!(!deletable_branch("", Some("abc123")));
2827 }
2828}
2829
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2830#[cfg(test)]
2831mod push_to_create_tests {
2832 use super::*;
2833
2834 #[test]
2835 fn a_pushed_repository_starts_private() {
2836 let owner: User = serde_json::from_value(serde_json::json!({ "id": "usr_1", "username": "ada" })).unwrap();
2837 let args = push_to_create(&owner, &RepoPath { namespace: "acme".into(), name: "site".into() });
2838 assert!(args.is_private);
2839 assert_eq!((args.namespace.as_str(), args.name.as_str()), ("acme", "site"));
2840 }
2841}
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca2842
2843#[cfg(test)]
2844mod deploy_key_git_tests {
2845 use super::*;
2846 use g1t_contracts::deploy_keys;
2847
2848 fn key(read_only: bool) -> User {
2849 deploy_keys::principal("wsp_acme", "acme", deploy_keys::access("dk_1", "CI", "acme/rocket", read_only))
2850 }
2851
2852 fn rocket(private: bool) -> Repo {
2853 serde_json::from_value(serde_json::json!({
2854 "id": "rep_rocket",
2855 "namespace": "acme",
2856 "name": "rocket",
2857 "description": null,
2858 "isPrivate": private,
2859 "ownerId": "usr_owner",
2860 "defaultBranch": "main",
2861 "forkOf": null,
2862 "protected": false,
2863 "createdAt": "",
2864 }))
2865 .unwrap()
2866 }
2867
2868 fn refusal(user: &User, repo: &str, write: bool, exists: bool) -> Option<String> {
2869 git_token_refusal(user.token.as_deref().unwrap(), repo, None, write, false, exists)
2870 }
2871
2872 #[test]
2873 fn a_read_only_deploy_key_clones_its_repository_and_never_pushes() {
2874 let user = key(true);
2875 assert_eq!(refusal(&user, "acme/rocket", false, true), None);
2876 assert!(refusal(&user, "acme/rocket", true, true).unwrap().contains("read-only"));
2877 // Its role is a workspace token's: it reads a private repository.
2878 assert!(registry::can_read(&rocket(true), &Some(user)));
2879 }
2880
2881 #[test]
2882 fn a_deploy_key_with_write_access_pushes_to_its_repository() {
2883 let user = key(false);
2884 assert_eq!(refusal(&user, "acme/rocket", true, true), None);
2885 assert!(registry::can_write(&rocket(true), &Some(user)));
2886 }
2887
2888 #[test]
2889 fn a_deploy_key_reaches_no_other_repository() {
2890 let user = key(false);
2891 for other in ["acme/booster", "other/rocket"] {
2892 for write in [false, true] {
2893 let why = refusal(&user, other, write, true).expect(other);
2894 assert!(why.contains("deploy key is for acme/rocket"), "{why}");
2895 }
2896 }
2897 // Not even a pull request's working copy of another repository.
2898 let token = user.token.as_deref().unwrap();
2899 assert!(git_token_refusal(token, "pulls/pr_1", Some("acme/booster"), false, false, true).is_some());
2900 }
2901
2902 #[test]
2903 fn a_deploy_key_never_creates_a_repository() {
2904 let why = refusal(&key(false), "acme/rocket", true, false).unwrap();
2905 assert!(why.contains("not there"), "{why}");
2906 }
2907}

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