Skip to content

g1t/services/repos/src/lib.rs

2,660 lines114,966 bytesCodeBlameRaw

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 branch 'worktree-agent-a2013627e5ea4ab13'16mod fallback;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily17mod forks;
Rust repos service with shipping; pull requests kept in the model18mod git_http;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look19mod git_ops;
Agents as a team: lifecycle, merge queue, billing and a new shell20mod import;
Rust repos service with shipping; pull requests kept in the model21mod land;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9722mod languages;
Branches and Tags pages, each file's last commit, and the branch menu on files23mod last_commits;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9724mod license;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look25mod lifecycle;
Search across all of g1t, Explore, and a command palette26mod listing;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily27mod meters;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look28mod mirror;
Merge branch 'worktree-agent-a2013627e5ea4ab13'29mod moves;
30mod namespaces;
Merge branch 'worktree-agent-a1b995daa94e4e1b7'31mod pack_cache;
Fast pages, required checks on the branch, self-hosted runners, honest incidents32mod pack_limits;
Pull requests from branches33mod refs;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms34mod refs_cache;
Rust repos service with shipping; pull requests kept in the model35mod registry;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily36mod resilience;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge37mod rule_facts;
38mod rules;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API39mod run_access;
40mod secret_scan;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily41mod shards;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms42mod shared;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge43mod signatures;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9744mod stats;
Rust repos service with shipping; pull requests kept in the model45mod store;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look46mod transfer;
Rust repos service with shipping; pull requests kept in the model47
Agents and memory, checks and conflicts, profiles, slug renames, custom domains48use g1t_contracts::events::{
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look49 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 member50 WorkspaceDeleting, WorkspaceRenamed, WorkspaceRestored,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains51};
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look52use g1t_contracts::access::{self, Capability};
Rust repos service with shipping; pull requests kept in the model53use g1t_contracts::repos::*;
RFC 3339 timestamps in identity and repos54use g1t_contracts::time::rfc3339;
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA55use 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 events56use g1t_kit::{args, now_ms, reply, rpc_method};
Diffs on attempts; hosted agent presented as the g1t agent57use std::collections::{HashMap, HashSet, VecDeque};
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms58use std::rc::Rc;
Rust repos service with shipping; pull requests kept in the model59
60use serde::Serialize;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look61use worker::{
62 Context, Env, Fetcher, MessageBatch, Method, Request, Response, Result, ScheduleContext, ScheduledEvent,
63 event,
64};
Rust repos service with shipping; pull requests kept in the model65
66use registry::{Registry, can_read, can_write, store_key};
67use store::{ArtifactsStore, GitRepo, GitStore, Scope};
68
Issues and pull requests replace intents and attempts69/// 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 daily70pub(crate) const PULLS_NAMESPACE: &str = "pulls";
Rust repos service with shipping; pull requests kept in the model71const MAX_TEXT_BYTES: usize = 512 * 1024;
Issues and pull requests replace intents and attempts72/// How far back a pull request may have forked and still be landed.
Rust repos service with shipping; pull requests kept in the model73const MAX_ANCESTRY: u32 = 1000;
Branches and Tags pages, each file's last commit, and the branch menu on files74/// The most tags a repository's Tags page reads and lists.
75const MAX_TAGS_READ: usize = 100;
76
77/// One path segment, percent-encoded for a cache key.
78fn urlencoding_segment(segment: &str) -> String {
79 segment
80 .bytes()
81 .map(|b| if b.is_ascii_alphanumeric() || b"-._~".contains(&b) { (b as char).to_string() } else { format!("%{b:02X}") })
82 .collect()
83}
Agents as a team: lifecycle, merge queue, billing and a new shell84const MAX_DESCRIPTION_CHARS: usize = 200;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look85pub(crate) const SOURCE: &str = "repos";
86pub(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 model87
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look88pub(crate) fn not_found<T>() -> Outcome<T> {
Rust repos service with shipping; pull requests kept in the model89 Outcome::fail(FailureCode::NotFound, "Repository not found.")
90}
91
92/// Decoded text, or `None` when the file is too large or looks binary.
Agents as a team: lifecycle, merge queue, billing and a new shell93/// Whether a ref is a full commit hash rather than a branch name.
94fn is_commit_hash(git_ref: &str) -> bool {
95 git_ref.len() == 40 && git_ref.bytes().all(|b| b.is_ascii_hexdigit())
96}
97
Rust repos service with shipping; pull requests kept in the model98fn text_of(bytes: Vec<u8>) -> Option<String> {
99 if bytes.len() > MAX_TEXT_BYTES || bytes.contains(&0) {
100 return None;
101 }
102 Some(String::from_utf8_lossy(&bytes).into_owned())
103}
104
105fn is_readme(name: &str) -> bool {
106 matches!(
107 name.to_lowercase().as_str(),
108 "readme" | "readme.md" | "readme.markdown" | "readme.txt"
109 )
110}
111
112/// Whether `ancestor` is reachable from the newest commit in `history`.
113///
114/// `history` is the first-parent chain, which is all the store lists; a fork
115/// that merged the target branch in has the target's head on a second
116/// parent, so the walk follows every parent.
117async fn descends_from<R: GitRepo>(repo: &R, history: &[Commit], ancestor: &str) -> Result<bool> {
118 let known: HashMap<&str, &[String]> = history
119 .iter()
120 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
121 .collect();
122 let mut seen = HashSet::new();
123 let mut queue: Vec<String> = history
124 .first()
125 .map(|c| c.hash.clone())
126 .into_iter()
127 .collect();
128 while let Some(hash) = queue.pop() {
129 if hash == ancestor {
130 return Ok(true);
131 }
132 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
133 continue;
134 }
135 match known.get(hash.as_str()) {
136 Some(parents) => queue.extend(parents.iter().cloned()),
137 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
138 }
139 }
140 Ok(false)
141}
142
Diffs on attempts; hosted agent presented as the g1t agent143/// The commit closest to the newest in `history` that is also in `shared`:
144/// where a fork and the repository it came from last agreed.
145async fn nearest_ancestor_in<R: GitRepo>(
146 repo: &R,
147 history: &[Commit],
148 shared: &HashSet<String>,
149) -> Result<Option<String>> {
150 let known: HashMap<&str, &[String]> = history
151 .iter()
152 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
153 .collect();
154 let mut seen = HashSet::new();
155 let mut queue: VecDeque<String> = history
156 .first()
157 .map(|c| c.hash.clone())
158 .into_iter()
159 .collect();
160 while let Some(hash) = queue.pop_front() {
161 if shared.contains(&hash) {
162 return Ok(Some(hash));
163 }
164 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
165 continue;
166 }
167 match known.get(hash.as_str()) {
168 Some(parents) => queue.extend(parents.iter().cloned()),
169 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
170 }
171 }
172 Ok(None)
173}
174
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily175thread_local! {
176 /// Targets' sides of mergeability, by head (coalesce.rs).
177 static TARGETS: std::cell::RefCell<coalesce::Memo<coalesce::TargetKey, Rc<coalesce::TargetSide>>> =
178 std::cell::RefCell::new(coalesce::Memo::new(coalesce::TARGET_TTL_MS, 32));
179 /// What targets changed between two trees.
180 static THEIRS: std::cell::RefCell<coalesce::Memo<coalesce::TheirsKey, (Vec<String>, bool)>> =
181 std::cell::RefCell::new(coalesce::Memo::new(coalesce::THEIRS_TTL_MS, 256));
182 /// What repositories hold, as read for a push's first request, for the
183 /// same push's second: a push's POST does not wait on the database.
184 static HELD: std::cell::RefCell<coalesce::Memo<String, u64>> =
185 std::cell::RefCell::new(coalesce::Memo::new(60_000, 512));
186}
187
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look188pub(crate) struct Repos<S: GitStore> {
Rust repos service with shipping; pull requests kept in the model189 registry: Registry,
190 store: S,
Events service in Rust, with RFC 3339 times and accurate push events191 events: Fetcher,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API192 /// Asked during a push which secrets have been allowed.
193 security: Option<Fetcher>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look194 /// Asked whether a workspace is on a plan, for its private storage.
195 billing: Option<Fetcher>,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge196 /// Says which rulesets hold for a change to a branch or tag, and keeps
197 /// how they judged it (rules.rs). `None` where it is not deployed: the
198 /// old protection flag then holds on push.
199 work: Option<Fetcher>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look200 /// Told when a repository moves, for the tokens of agents at work on it.
201 identity: Option<Fetcher>,
202 /// What a free workspace's private repositories may hold.
203 free_private_bytes: i64,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily204 /// Days a pull request's working copy is kept after it settles (forks.rs).
205 pub(crate) fork_days: u64,
206 /// The most a repository may hold (pack_limits.rs), and what happens
207 /// to a push too large to scan.
208 repo_limit: u64,
209 large_pushes: git_http::LargePushes,
Merge branch 'worktree-agent-a2013627e5ea4ab13'210 /// Which git store namespace new repositories go in (shards.rs), and
211 /// the most each should hold (`ARTIFACTS_NAMESPACE_LIMITS`).
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily212 placement: shards::Placement,
Merge branch 'worktree-agent-a2013627e5ea4ab13'213 limits: HashMap<String, u64>,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms214 /// What isolates share: answers that list refs (refs_cache.rs).
215 shared: Option<Rc<shared::Shared>>,
Merge branch 'worktree-agent-a1b995daa94e4e1b7'216 /// Packs for fresh clones (pack_cache.rs); `None` without the bucket.
Merge branch 'worktree-agent-aaf03bdceac799c89'217 packs: Option<Rc<pack_cache::Packs>>,
Rust repos service with shipping; pull requests kept in the model218}
219
220impl<S: GitStore> Repos<S> {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms221 /// Records that the refs of the repository with this id changed, once
222 /// they have, so that the answers kept that list them go stale (see
223 /// refs_cache.rs). Everything that changes a repository's refs calls
224 /// this after it (`every_ref_writer_records_the_change` checks). A
225 /// failure is logged: the change itself happened, and what was kept
226 /// expires within `refs_cache::TTL_SECONDS` regardless.
227 pub(crate) async fn refs_moved(&self, repo_id: &str) {
228 if let Err(error) = self.registry.refs_moved(repo_id).await {
229 worker::console_error!("refs of {repo_id} changed but not recorded: {error}");
230 }
231 }
232
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look233 pub(crate) async fn publish<T: Serialize>(&self, event: NewEvent<T>) -> Result<()> {
Events service in Rust, with RFC 3339 times and accurate push events234 g1t_kit::call(
235 &self.events,
236 "publish",
237 &Publish {
238 events: vec![event],
239 },
240 )
241 .await
Rust repos service with shipping; pull requests kept in the model242 }
243
Members can read a private repository's pull request forks244 /// Whether the viewer may read `repo`. A pull request's fork of a
245 /// private repository can be read by everyone who can read that
246 /// repository, so its members can review and check out the change, as
247 /// well as by whoever opened the pull request.
248 async fn may_read(&self, repo: &Repo, viewer: &Viewer) -> Result<bool> {
249 if can_read(repo, viewer) {
250 return Ok(true);
251 }
252 let Some(source_id) = &repo.fork_of else {
253 return Ok(false);
254 };
Rust repos service with shipping; pull requests kept in the model255 Ok(self
256 .registry
Members can read a private repository's pull request forks257 .by_id(source_id)
Rust repos service with shipping; pull requests kept in the model258 .await?
Members can read a private repository's pull request forks259 .is_some_and(|source| can_read(&source, viewer)))
Rust repos service with shipping; pull requests kept in the model260 }
261
Members can read a private repository's pull request forks262 /// `repo`, if there is one and the viewer may read it.
263 async fn visible(&self, repo: Option<Repo>, viewer: &Viewer) -> Result<Option<Repo>> {
264 Ok(match repo {
265 Some(repo) if self.may_read(&repo, viewer).await? => Some(repo),
266 _ => None,
267 })
268 }
269
270 /// 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 look271 pub(crate) async fn readable(&self, path: &RepoPath, viewer: &Viewer) -> Result<Option<Repo>> {
Members can read a private repository's pull request forks272 self.visible(self.registry.by_path(path).await?, viewer)
273 .await
274 }
275
Rust repos service with shipping; pull requests kept in the model276 async fn get(&self, a: GetArgs) -> Result<Outcome<Repo>> {
277 Ok(self
278 .readable(&a.path, &a.viewer)
279 .await?
280 .map_or_else(not_found, Outcome::Ok))
281 }
282
283 async fn get_by_id(&self, a: GetByIdArgs) -> Result<Outcome<Repo>> {
284 Ok(self
Members can read a private repository's pull request forks285 .visible(self.registry.by_id(&a.id).await?, &a.viewer)
Rust repos service with shipping; pull requests kept in the model286 .await?
287 .map_or_else(not_found, Outcome::Ok))
288 }
289
Agents as a team: lifecycle, merge queue, billing and a new shell290 async fn update(&self, a: UpdateArgs) -> Result<Outcome<Repo>> {
291 let viewer = Some(a.actor.clone());
292 let Some(repo) = self.readable(&a.path, &viewer).await? else {
293 return Ok(not_found());
294 };
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA295 // Its details take Maintain; its protection, Admin; who can see it,
296 // 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 look297 let protection_changes = a.protected.is_some_and(|protected| protected != repo.protected);
298 let details_change = a.description.is_some() || a.website.is_some() || a.topics.is_some();
299 let mut needed = Vec::new();
300 if details_change || !protection_changes {
301 needed.push(Capability::ManageSettings);
302 }
303 if protection_changes {
304 needed.push(Capability::ManageProtection);
305 }
306 let full_name = format!("{}/{}", repo.namespace, repo.name);
307 if repo.fork_of.is_some() {
308 return Ok(Outcome::fail(FailureCode::Forbidden, access::needs(Capability::ManageSettings, &full_name)));
309 }
310 if let Some(missing) = needed.into_iter().find(|capability| !registry::can(&repo, &viewer, *capability)) {
311 return Ok(Outcome::fail(FailureCode::Forbidden, access::needs(missing, &full_name)));
Agents as a team: lifecycle, merge queue, billing and a new shell312 }
313 if !a.actor.verified {
314 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
315 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look316 if let Some((code, message)) = lifecycle::archived_refusal(&repo) {
317 return Ok(Outcome::fail(code, message));
318 }
Agents as a team: lifecycle, merge queue, billing and a new shell319 let description = match a.description {
320 Some(text) => Some(
321 text.trim()
322 .chars()
323 .take(MAX_DESCRIPTION_CHARS)
324 .collect::<String>(),
325 )
326 .filter(|text| !text.is_empty()),
327 None => repo.description.clone(),
328 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look329 let website = match a.website.as_deref() {
330 Some(text) => match clean_website(text) {
331 Ok(website) => website,
332 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
333 },
334 None => repo.website.clone(),
335 };
336 // Who can see it is an owner's to change, and a free workspace's
337 // storage may not take it private: see lifecycle.rs.
338 let wants_private = a.is_private.filter(|private| *private != repo.is_private);
339 if wants_private.is_some()
340 && let Err((code, message)) = lifecycle::admin_only(
341 lifecycle::Asker::on(&a.actor, &repo),
342 &repo.namespace,
343 "change the visibility of",
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA344 Capability::ChangeVisibility,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look345 )
346 {
347 return Ok(Outcome::fail(code, message));
348 }
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA349 if let Some(private) = wants_private
350 && let Some(why) = lifecycle::visibility_refusal(&a.actor, &repo, private)
351 {
352 return Ok(Outcome::fail(FailureCode::Forbidden, why));
353 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look354 let is_private = repo.is_private;
Agents as a team: lifecycle, merge queue, billing and a new shell355 let protected = a.protected.unwrap_or(repo.protected);
Search across all of g1t, Explore, and a command palette356 let topics = match &a.topics {
357 Some(topics) => match clean_topics(topics) {
358 Ok(topics) => topics,
359 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
360 },
361 None => repo.topics.clone(),
362 };
Agents as a team: lifecycle, merge queue, billing and a new shell363 self.registry
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look364 .update(&repo.id, description.as_deref(), protected, &topics, website.as_deref())
Agents as a team: lifecycle, merge queue, billing and a new shell365 .await?;
Search across all of g1t, Explore, and a command palette366 let updated = Repo {
Agents as a team: lifecycle, merge queue, billing and a new shell367 description,
368 is_private,
369 protected,
Search across all of g1t, Explore, and a command palette370 topics,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look371 website,
Agents as a team: lifecycle, merge queue, billing and a new shell372 ..repo
Search across all of g1t, Explore, and a command palette373 };
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge374 // Whether the default branch takes only pull requests is now its
375 // branch protection ruleset's to say (work's rulesets.rs).
376 if let (Some(protected), Some(work)) = (a.protected, &self.work) {
377 #[derive(Serialize)]
378 struct RequirePullRequest<'a> {
379 repo: &'a Repo,
380 protected: bool,
381 actor: &'a User,
382 }
383 let set: Result<Outcome<bool>> =
384 g1t_kit::call(work, "set_requires_pull_request", &RequirePullRequest { repo: &updated, protected, actor: &a.actor }).await;
385 match set {
386 Ok(Outcome::Ok(_)) => {}
387 Ok(Outcome::Fail(failure)) => return Ok(Outcome::Fail(failure)),
388 Err(error) => return Err(error),
389 }
390 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look391 if let Some(private) = wants_private {
392 return self.change_visibility(updated, private, &a.actor, a.surface).await;
393 }
394 let visibility_changed = false;
Search across all of g1t, Explore, and a command palette395 // Search and anything else that shows the repository hears of it;
396 // a change of visibility is announced on its own as well, so that
397 // what was public stops being shown at once.
398 self.publish(NewEvent {
399 kind: "repo.updated",
400 source: SOURCE,
401 repo_id: Some(updated.id.clone()),
402 actor: Some(a.actor.id.clone()),
403 data: RepoUpdated {
404 repo_id: updated.id.clone(),
405 namespace: updated.namespace.clone(),
406 name: updated.name.clone(),
407 is_private,
408 visibility_changed,
409 },
410 })
411 .await?;
412 Ok(Outcome::Ok(updated))
413 }
414
415 /// The repository with this id, if it is not a fork, and its store.
416 async fn stored(&self, repo_id: &str) -> Result<Option<S::Repo>> {
417 match self.registry.by_id(repo_id).await? {
418 Some(repo) if repo.fork_of.is_none() => Ok(Some(self.store.open(&store_key(&repo)).await?)),
419 _ => Ok(None),
420 }
421 }
422
423 async fn list_files(&self, a: ListFilesArgs) -> Result<FileList> {
424 let Some(repo) = self.registry.by_id(&a.repo_id).await?.filter(|repo| repo.fork_of.is_none()) else {
425 return Ok(FileList::default());
426 };
427 let git = self.store.open(&store_key(&repo)).await?;
428 let head = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
429 listing::list(&git, None, &head, &a.skip_dirs, a.limit).await
430 }
431
432 async fn changed_files(&self, a: ChangedFilesArgs) -> Result<FileList> {
433 let Some(git) = self.stored(&a.repo_id).await? else {
434 return Ok(FileList::default());
435 };
436 listing::list(&git, a.base.as_deref(), &a.head, &a.skip_dirs, a.limit).await
437 }
438
Composer from the workspace's own repositories, and go get from g1t.sh439 /// Branches and tags with their commits, for g1t's own services.
440 async fn refs_of(&self, a: RefsArgs) -> Result<Option<RepoRefs>> {
441 let Some(repo) = self.registry.by_id(&a.repo_id).await?.filter(|repo| repo.fork_of.is_none()) else {
442 return Ok(None);
443 };
444 let git = self.store.open(&store_key(&repo)).await?;
445 let access = git.access(Scope::Read).await?;
446 let refs = refs::heads_and_tags(refs::all(&access).await?)
447 .into_iter()
448 .map(|(name, commit)| GitRefEntry { name, commit })
449 .collect();
450 Ok(Some(RepoRefs { repo, refs }))
451 }
452
453 async fn raw_file(&self, a: RawFileArgs) -> Result<Option<RawFile>> {
454 use base64::Engine;
455 let Some(git) = self.stored(&a.repo_id).await? else {
456 return Ok(None);
457 };
458 Ok(git
459 .read_file(&a.git_ref, &a.path)
460 .await?
461 .filter(|bytes| bytes.len() <= a.max_bytes as usize)
462 .map(|bytes| RawFile { size: bytes.len() as u64, data: base64::engine::general_purpose::STANDARD.encode(bytes) }))
463 }
464
465 async fn raw_blobs(&self, a: RawBlobsArgs) -> Result<Vec<RawBlob>> {
466 use base64::Engine;
467 let Some(git) = self.stored(&a.repo_id).await? else {
468 return Ok(Vec::new());
469 };
470 let hashes: Vec<&String> = a.hashes.iter().take(MAX_READ_BLOBS).collect();
471 let mut out = Vec::with_capacity(hashes.len());
472 // A few at a time, as listing::read does: each is a round trip.
473 for group in hashes.chunks(8) {
474 let read = futures_util::future::try_join_all(group.iter().map(|hash| git.read_blob(hash))).await?;
475 for (hash, bytes) in group.iter().zip(read) {
476 let size = bytes.as_ref().map_or(0, |bytes| bytes.len() as u64);
477 let data = bytes
478 .filter(|bytes| bytes.len() <= a.max_bytes as usize)
479 .map(|bytes| base64::engine::general_purpose::STANDARD.encode(bytes));
480 out.push(RawBlob { hash: (*hash).clone(), size, data });
481 }
482 }
483 Ok(out)
484 }
485
Search across all of g1t, Explore, and a command palette486 async fn read_blobs(&self, a: ReadBlobsArgs) -> Result<Vec<BlobText>> {
487 let Some(git) = self.stored(&a.repo_id).await? else {
488 return Ok(Vec::new());
489 };
490 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 shell491 }
492
Rust repos service with shipping; pull requests kept in the model493 async fn create(&self, a: CreateArgs) -> Result<Outcome<Repo>> {
494 if !a.owner.verified {
495 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
496 }
497 let name = a.name.trim().to_lowercase();
498 if !is_valid_repo_name(&name) {
499 return Ok(Outcome::fail(
500 FailureCode::Invalid,
501 "Use letters, digits, dots, hyphens and underscores only.",
502 ));
503 }
Workspaces own repositories504 let namespace = a.namespace.trim().to_lowercase();
505 if namespace.is_empty() {
Rust repos service with shipping; pull requests kept in the model506 return Ok(Outcome::fail(
507 FailureCode::Invalid,
Workspaces own repositories508 "Say which workspace to create the repository in.",
509 ));
510 }
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA511 let Some(role) = a.owner.role_in(&namespace) else {
Workspaces own repositories512 return Ok(Outcome::fail(
513 FailureCode::Forbidden,
514 "You are not a member of that workspace.",
Rust repos service with shipping; pull requests kept in the model515 ));
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA516 };
517 // Who may create which: the workspace's member privileges. A
518 // workspace's own token acts as an owner would.
519 let role = if a.owner.kind == PrincipalKind::Workspace { Role::Owner } else { role };
520 if let Some(why) = a.owner.privileges_in(&namespace).creation_refusal(role, a.is_private, &namespace) {
521 return Ok(Outcome::fail(FailureCode::Forbidden, why));
Rust repos service with shipping; pull requests kept in the model522 }
Workspaces own repositories523 let path = RepoPath { namespace, name };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look524 match self.registry.by_path_any(&path).await? {
525 Some((_, None)) => {
526 return Ok(Outcome::fail(
527 FailureCode::Conflict,
528 "That workspace already has a repository with that name.",
529 ));
530 }
531 Some((_, Some(_))) => {
532 return Ok(Outcome::fail(
533 FailureCode::Conflict,
534 format!(
535 "{}/{} 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.",
536 path.namespace, path.name
537 ),
538 ));
539 }
540 None => {}
541 }
542 // With a credential (a GitHub App installation's token), everything
543 // is copied: every branch and tag. See mirror.rs.
544 let mut credentialed = None;
545 if let (Some(url), Some(token)) = (a.import_url.as_deref(), a.import_token.as_deref()) {
546 let Some(url) = import::clean_url(url) else {
547 return Ok(Outcome::fail(FailureCode::Invalid, "That is not an https repository address."));
548 };
549 let source = mirror::Endpoint::github(&url, token);
550 match mirror::probe(&source).await? {
551 Ok(advertised) => credentialed = Some((source, advertised)),
552 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
553 }
Rust repos service with shipping; pull requests kept in the model554 }
Agents as a team: lifecycle, merge queue, billing and a new shell555 // An import is fetched before anything is created, so that an
556 // address that does not work leaves nothing behind.
557 let mut imported = None;
558 if let Some(url) = a
559 .import_url
560 .as_deref()
561 .map(str::trim)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look562 .filter(|url| !url.is_empty() && credentialed.is_none())
Agents as a team: lifecycle, merge queue, billing and a new shell563 {
564 let Some(url) = import::clean_url(url) else {
565 return Ok(Outcome::fail(
566 FailureCode::Invalid,
567 "Give the https address of a public repository, such as https://github.com/owner/repo.",
568 ));
569 };
570 let remote = match import::discover(&url).await? {
571 Ok(remote) => remote,
572 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
573 };
A public import copies every branch and tag, so an imported library keeps its releases574 imported = Some((remote, url));
Agents as a team: lifecycle, merge queue, billing and a new shell575 }
Rust repos service with shipping; pull requests kept in the model576 let now = now_ms();
577 let repo = Repo {
578 id: new_id("rep", now),
579 namespace: path.namespace,
580 name: path.name,
581 description: a
582 .description
583 .map(|text| text.trim().to_owned())
584 .filter(|text| !text.is_empty()),
585 is_private: a.is_private,
586 owner_id: a.owner.id.clone(),
Agents as a team: lifecycle, merge queue, billing and a new shell587 default_branch: imported
588 .as_ref()
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look589 .map(|(remote, _)| remote.branch.clone())
590 .or_else(|| credentialed.as_ref().and_then(|(_, advertised)| advertised.default_branch()))
591 .unwrap_or_else(|| "main".to_owned()),
Rust repos service with shipping; pull requests kept in the model592 fork_of: None,
Agents as a team: lifecycle, merge queue, billing and a new shell593 protected: false,
RFC 3339 timestamps in identity and repos594 created_at: rfc3339(now),
Search across all of g1t, Explore, and a command palette595 topics: Vec::new(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look596 website: None,
597 archived_at: None,
Rust repos service with shipping; pull requests kept in the model598 };
Merge branch 'worktree-agent-a2013627e5ea4ab13'599 let namespace = match self.place(&repo).await? {
600 Ok(namespace) => namespace,
601 Err(unplaced) => return Ok(Outcome::fail(FailureCode::Conflict, unplaced.message())),
602 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily603 self.registry
604 .claim_store_key(&repo, namespace.as_deref(), &self.store.default_namespace())
605 .await?;
Rust repos service with shipping; pull requests kept in the model606 self.store
607 .create(
608 &store_key(&repo),
609 repo.description.as_deref(),
610 &repo.default_branch,
611 )
612 .await?;
613 self.registry.insert(&repo).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look614 // A repository that was transferred away from this path stops
615 // redirecting here.
616 self.registry
617 .drop_redirect(&RepoPath {
618 namespace: repo.namespace.clone(),
619 name: repo.name.clone(),
620 })
621 .await?;
A public import copies every branch and tag, so an imported library keeps its releases622 // Every branch and tag the import made, announced as pushes.
623 let mut pushed: Vec<(String, String)> = Vec::new();
624 // A public repository, read with no credential: every branch and
625 // tag is copied too, the default branch the one its HEAD names.
626 if let Some((_, url)) = imported {
Agents as a team: lifecycle, merge queue, billing and a new shell627 let access = self
628 .store
629 .open(&store_key(&repo))
630 .await?
631 .access(Scope::Write)
632 .await?;
A public import copies every branch and tag, so an imported library keeps its releases633 let target = mirror::Endpoint::bearer(&access.remote, &access.token);
634 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 200ms635 self.refs_moved(&repo.id).await;
A public import copies every branch and tag, so an imported library keeps its releases636 match copied {
637 Ok(copied) => pushed = mirror::import_pushes(&copied.updated, &repo.default_branch),
638 Err(reason) => {
639 self.registry.remove(&repo.id).await?;
640 return Ok(Outcome::fail(
641 FailureCode::Invalid,
642 format!("The repository could not be stored: {reason}"),
643 ));
644 }
Agents as a team: lifecycle, merge queue, billing and a new shell645 }
646 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look647 if let Some((source, _)) = credentialed {
648 let access = self
649 .store
650 .open(&store_key(&repo))
651 .await?
652 .access(Scope::Write)
653 .await?;
654 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 200ms655 let copied = mirror::copy(&source, &target, mirror::Prune::Yes).await?;
656 self.refs_moved(&repo.id).await;
657 match copied {
A public import copies every branch and tag, so an imported library keeps its releases658 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 look659 Err(reason) => {
660 self.registry.remove(&repo.id).await?;
661 return Ok(Outcome::fail(
662 FailureCode::Invalid,
663 format!("The repository could not be copied: {reason}"),
664 ));
665 }
666 }
667 }
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA668 // Whoever creates a repository is an Admin of it, as a role given
669 // to them on it, whatever the workspace's base permission.
670 if a.owner.kind == PrincipalKind::User
671 && let Some(identity) = &self.identity
672 {
673 let granted: Result<bool> = g1t_kit::call(
674 identity,
675 "grant_creator",
676 &g1t_contracts::members::GrantCreatorArgs {
677 repo_id: repo.id.clone(),
678 namespace: repo.namespace.clone(),
679 name: repo.name.clone(),
680 user_id: a.owner.id.clone(),
681 },
682 )
683 .await;
684 if let Err(error) = granted {
685 worker::console_error!("creator of {} not given Admin: {error}", repo.id);
686 }
687 }
Rust repos service with shipping; pull requests kept in the model688 self.publish(NewEvent {
689 kind: "repo.created",
690 source: SOURCE,
691 repo_id: Some(repo.id.clone()),
692 actor: Some(a.owner.id),
693 data: RepoCreated {
694 repo_id: repo.id.clone(),
695 namespace: repo.namespace.clone(),
696 name: repo.name.clone(),
697 is_private: repo.is_private,
698 },
699 })
700 .await?;
A public import copies every branch and tag, so an imported library keeps its releases701 for (git_ref, head) in &pushed {
702 self.publish_push(&repo, git_ref, None, head, None).await?;
Agents as a team: lifecycle, merge queue, billing and a new shell703 }
Rust repos service with shipping; pull requests kept in the model704 Ok(Outcome::Ok(repo))
705 }
706
Merge branch 'worktree-agent-a2013627e5ea4ab13'707 /// Where a workspace keeps its data, asked of identity only when an EU
708 /// namespace is configured: without one, every workspace's
709 /// repositories go anywhere and identity is never asked.
710 async fn residency_of(&self, workspace: &str) -> Result<shards::Residency> {
711 if self.placement.eu.is_none() {
712 return Ok(shards::Residency::Anywhere);
713 }
714 let Some(identity) = &self.identity else {
715 return Ok(shards::Residency::Anywhere);
716 };
717 let residency: Option<g1t_contracts::identity::DataResidency> = g1t_kit::call(
718 identity,
719 "workspace_residency",
720 &g1t_contracts::identity::SlugArgs { slug: workspace.to_owned() },
721 )
722 .await?;
723 Ok(match residency {
724 Some(g1t_contracts::identity::DataResidency::Eu) => shards::Residency::Eu,
725 _ => shards::Residency::Anywhere,
726 })
727 }
728
729 /// How each bound namespace stands (namespaces.rs).
730 async fn standings(&self) -> Result<Vec<namespaces::Standing>> {
731 let bound = self.store.namespaces();
732 let default = self.store.default_namespace();
733 let now = now_ms();
734 let config = namespaces::Configured {
735 bound: &bound,
736 default: &default,
737 placement: &self.placement,
738 limits: &self.limits,
739 on_fallback: &|namespace| self.store.on_fallback(&shards::compose(Some(namespace), "x", &default)),
740 writable: &|namespace| self.store.writable(namespace),
741 breaker_open: &|namespace| resilience::open_now(namespace, now),
742 };
743 let (held, recent) = futures_util::future::join(namespaces::held(&self.registry.db), namespaces::recent(&self.registry.db, now)).await;
744 Ok(namespaces::standings(&config, &held?, &recent?))
745 }
746
747 /// The namespace a new repository goes in (shards.rs): its workspace's
748 /// residency, then how each namespace stands, read only when there is
749 /// a choice to make. `Ok(None)` for the default.
750 async fn place(&self, repo: &Repo) -> Result<std::result::Result<Option<String>, shards::Unplaced>> {
751 let residency = self.residency_of(&repo.namespace).await?;
752 let bound = self.store.namespaces();
753 let loads = if residency == shards::Residency::Anywhere && !self.placement.needs_loads(&bound) {
754 // One namespace to choose from at most: nothing to read.
755 bound
756 .iter()
757 .map(|namespace| shards::Load {
758 namespace: namespace.clone(),
759 bound: true,
760 writable: self.store.writable(namespace),
761 ..shards::Load::default()
762 })
763 .collect()
764 } else {
765 let default = self.store.default_namespace();
766 let now = now_ms();
767 let config = namespaces::Configured {
768 bound: &bound,
769 default: &default,
770 placement: &self.placement,
771 limits: &self.limits,
772 on_fallback: &|namespace| self.store.on_fallback(&shards::compose(Some(namespace), "x", &default)),
773 writable: &|namespace| self.store.writable(namespace),
774 breaker_open: &|namespace| resilience::open_now(namespace, now),
775 };
776 namespaces::loads(&self.registry.db, &config, now).await?
777 };
778 Ok(self.placement.choose(&repo.id, residency, &loads))
779 }
780
781 /// `storage_options`: what a workspace may choose about where its
782 /// repositories are kept.
783 fn storage_options(&self) -> StorageOptions {
784 let bound = self.store.namespaces();
785 StorageOptions { eu_available: self.placement.eu_available(&bound, |namespace| self.store.writable(namespace)) }
786 }
787
Rust repos service with shipping; pull requests kept in the model788 async fn tree(&self, a: TreeArgs) -> Result<Outcome<TreeView>> {
789 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
790 return Ok(not_found());
791 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily792 let git = self.read_git(&repo).await?;
Rust repos service with shipping; pull requests kept in the model793 let git_ref = a
794 .git_ref
795 .clone()
796 .unwrap_or_else(|| repo.default_branch.clone());
797
798 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
799 // An unknown ref is an error; a repo with no commits is just empty.
800 if a.git_ref.is_some() {
801 return Ok(Outcome::fail(
802 FailureCode::NotFound,
803 "No such branch, tag or commit.",
804 ));
805 }
806 return Ok(Outcome::Ok(TreeView {
807 repo,
808 git_ref,
809 path: a.tree_path,
810 head: None,
811 entries: Vec::new(),
812 readme: None,
813 }));
814 };
815
816 let no_directory = || Outcome::fail(FailureCode::NotFound, "No such directory.");
817 let mut entries = git.read_tree(&head.tree_hash).await?;
818 for segment in a.tree_path.split('/').filter(|segment| !segment.is_empty()) {
819 let next = entries.as_ref().and_then(|entries| {
820 entries
821 .iter()
822 .find(|entry| entry.name == segment && entry.kind == EntryKind::Tree)
823 });
824 let Some(next) = next else {
825 return Ok(no_directory());
826 };
827 entries = git.read_tree(&next.hash).await?;
828 }
829 let Some(mut entries) = entries else {
830 return Ok(no_directory());
831 };
832 // Directories first, then by name.
833 entries.sort_by(|a, b| {
834 (b.kind == EntryKind::Tree)
835 .cmp(&(a.kind == EntryKind::Tree))
836 .then_with(|| a.name.cmp(&b.name))
837 });
838
839 let readme_entry = entries
840 .iter()
841 .find(|entry| entry.kind == EntryKind::Blob && is_readme(&entry.name));
842 let readme = match readme_entry {
843 Some(entry) => git.read_blob(&entry.hash).await?.map(|bytes| Readme {
844 name: entry.name.clone(),
845 text: text_of(bytes),
846 }),
847 None => None,
848 };
849 Ok(Outcome::Ok(TreeView {
850 repo,
851 git_ref,
852 path: a.tree_path,
853 head: Some(head),
854 entries,
855 readme,
856 }))
857 }
858
859 async fn blob(&self, a: BlobArgs) -> Result<Outcome<BlobView>> {
860 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
861 return Ok(not_found());
862 };
863 let bytes = if a.file_path.is_empty() {
864 None
865 } else {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily866 let git = self.read_git(&repo).await?;
Rust repos service with shipping; pull requests kept in the model867 git.read_file(&a.git_ref, &a.file_path).await?
868 };
869 let Some(bytes) = bytes else {
870 return Ok(Outcome::fail(FailureCode::NotFound, "No such file."));
871 };
872 Ok(Outcome::Ok(BlobView {
873 repo,
874 git_ref: a.git_ref,
875 path: a.file_path,
876 size: bytes.len() as u64,
877 text: text_of(bytes),
878 }))
879 }
880
Agents as a team: lifecycle, merge queue, billing and a new shell881 async fn blame(&self, a: BlameArgs) -> Result<Outcome<Blame>> {
882 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
883 return Ok(not_found());
884 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily885 let git = self.read_git(&repo).await?;
Agents as a team: lifecycle, merge queue, billing and a new shell886 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
887 Ok(match blame::blame(&git, &git_ref, &a.file_path).await? {
888 Some(blame) => Outcome::Ok(blame),
889 None => not_found(),
890 })
891 }
892
Rust repos service with shipping; pull requests kept in the model893 async fn log(&self, a: LogArgs) -> Result<Outcome<Vec<Commit>>> {
894 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
895 return Ok(not_found());
896 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily897 let git = self.read_git(&repo).await?;
Rust repos service with shipping; pull requests kept in the model898 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
899 Ok(Outcome::Ok(git.log(&git_ref, a.limit).await?))
900 }
901
Branches and Tags pages, each file's last commit, and the branch menu on files902 /// Which commit last changed each entry of a directory. Kept in this
903 /// colo's cache by repository, head commit and path: a commit's history
904 /// never changes, so an answer is good for as long as it is kept.
905 async fn last_commits(&self, a: g1t_contracts::repos::LastCommitsArgs) -> Result<Outcome<g1t_contracts::repos::LastCommits>> {
906 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
907 return Ok(not_found());
908 };
909 let git = self.read_git(&repo).await?;
910 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
911 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
912 return Ok(Outcome::fail(FailureCode::NotFound, "No such branch, tag or commit."));
913 };
914 let key = format!(
915 "https://last-commits.g1t.internal/{}/{}/{}",
916 repo.id,
917 head.hash,
918 a.tree_path.split('/').map(urlencoding_segment).collect::<Vec<_>>().join("/")
919 );
920 let cache = worker::Cache::default();
921 if let Ok(Some(mut kept)) = cache.get(key.as_str(), false).await {
922 if let Ok(found) = kept.json::<g1t_contracts::repos::LastCommits>().await {
923 return Ok(Outcome::Ok(found));
924 }
925 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait926 // Asked with a budget: past it, what was found so far, not kept.
A last-commits walk answers within 2.5 s with what it has, and keeps only finished answers927 let started = worker::Date::now().as_millis();
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait928 let budget = a.budget_ms;
929 let out_of_time = move || budget.is_some_and(|budget| worker::Date::now().as_millis().saturating_sub(started) > budget);
A last-commits walk answers within 2.5 s with what it has, and keeps only finished answers930 let (entries, complete) = last_commits::last_commits(&git, &head.hash, &a.tree_path, &out_of_time).await?;
931 let stopped = out_of_time();
Branches and Tags pages, each file's last commit, and the branch menu on files932 let found = g1t_contracts::repos::LastCommits { entries, complete };
A last-commits walk answers within 2.5 s with what it has, and keeps only finished answers933 if stopped && !found.complete {
934 return Ok(Outcome::Ok(found));
935 }
Branches and Tags pages, each file's last commit, and the branch menu on files936 if let Ok(mut response) = worker::Response::from_json(&found) {
937 let _ = response.headers_mut().set("cache-control", "max-age=604800");
938 let _ = cache.put(key.as_str(), response).await;
939 }
940 Ok(Outcome::Ok(found))
941 }
942
943 /// The repository's tags, newest commit first, at most 100.
944 async fn tags(&self, a: g1t_contracts::repos::TagsArgs) -> Result<Outcome<Vec<g1t_contracts::repos::Tag>>> {
945 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
946 return Ok(not_found());
947 };
948 let git = self.store.open(&store_key(&repo)).await?;
949 let access = git.access(Scope::Read).await?;
950 let named: Vec<(String, String)> = refs::heads_and_tags(refs::all(&access).await?)
951 .into_iter()
952 .filter_map(|(name, hash)| name.strip_prefix("refs/tags/").map(|tag| (tag.to_owned(), hash)))
953 .collect();
954 let read = self.read_git(&repo).await?;
955 let commits = futures_util::future::join_all(named.iter().take(MAX_TAGS_READ).map(|(_, hash)| read.log(hash, 1))).await;
956 let mut tags: Vec<g1t_contracts::repos::Tag> = named
957 .into_iter()
958 .zip(commits.into_iter().map(|found| found.ok().and_then(|list| list.into_iter().next())).chain(std::iter::repeat(None)))
959 .map(|((name, _), commit)| g1t_contracts::repos::Tag { name, commit })
960 .collect();
961 tags.sort_by(|a, b| {
962 let at = |tag: &g1t_contracts::repos::Tag| tag.commit.as_ref().map(|c| c.authored_at.clone()).unwrap_or_default();
963 at(b).cmp(&at(a)).then_with(|| b.name.cmp(&a.name))
964 });
965 tags.truncate(MAX_TAGS_READ);
966 Ok(Outcome::Ok(tags))
967 }
968
Pull requests from branches969 /// The repository's branches, default branch first.
970 async fn branches(&self, a: BranchesArgs) -> Result<Outcome<Vec<Branch>>> {
971 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
972 return Ok(not_found());
973 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily974 let mut branches = self.read_git(&repo).await?.branches().await?;
Pull requests from branches975 branches.sort_by_key(|branch| branch.name != repo.default_branch);
976 Ok(Outcome::Ok(branches))
977 }
978
Agents as a team: lifecycle, merge queue, billing and a new shell979 /// Whether a pull request's source lacks commits that the branch it
980 /// would merge into has.
981 async fn behind(&self, a: BehindArgs) -> Result<bool> {
982 let Some(source) = self.registry.by_id(&a.source_id).await? else {
983 return Ok(false);
984 };
985 let target = match &source.fork_of {
986 Some(id) => self.registry.by_id(id).await?,
987 None => Some(source.clone()),
988 };
989 let Some(target) = target else {
990 return Ok(false);
991 };
992 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 bar993 let target_branch = a.target_branch.unwrap_or_else(|| target.default_branch.clone());
Agents as a team: lifecycle, merge queue, billing and a new shell994 let target_head = self
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily995 .read_git(&target)
Agents as a team: lifecycle, merge queue, billing and a new shell996 .await?
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar997 .log(&target_branch, 1)
Agents as a team: lifecycle, merge queue, billing and a new shell998 .await?
999 .into_iter()
1000 .next()
1001 .map(|commit| commit.hash);
1002 let Some(target_head) = target_head else {
1003 return Ok(false);
1004 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1005 let source_git = self.read_git(&source).await?;
Agents as a team: lifecycle, merge queue, billing and a new shell1006 let history = source_git.log(&branch, MAX_ANCESTRY).await?;
1007 if history.is_empty() {
1008 return Ok(false);
1009 }
1010 Ok(!descends_from(&source_git, &history, &target_head).await?)
1011 }
1012
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1013 /// The files a pull request's source and the default branch it would
1014 /// merge into each changed since they last agreed. Where the two lists
1015 /// share no file, the merge cannot conflict; where they do, it may.
1016 async fn divergence(&self, a: BehindArgs) -> Result<Option<Divergence>> {
1017 let Some(source) = self.registry.by_id(&a.source_id).await? else {
1018 return Ok(None);
1019 };
1020 let target = match &source.fork_of {
1021 Some(id) => self.registry.by_id(id).await?,
1022 None => Some(source.clone()),
1023 };
1024 let Some(target) = target else {
1025 return Ok(None);
1026 };
1027 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 bar1028 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 daily1029 let source_git = self.read_git(&source).await?;
1030 let target_git = self.read_git(&target).await?;
1031 // The target's side is the same for every pull request into it, and
1032 // worked out once per head (coalesce.rs).
1033 let (history, side) = futures_util::future::try_join(
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1034 source_git.log(&branch, MAX_ANCESTRY),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1035 self.target_side(&target, &target_git, &target_branch),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1036 )
1037 .await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1038 let target_history = &side.history;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1039 let (Some(head), Some(base)) = (history.first(), target_history.first()) else {
1040 return Ok(None);
1041 };
1042 let behind = !descends_from(&source_git, &history, &base.hash).await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1043 let merge_base = nearest_ancestor_in(&source_git, &history, &side.shared).await?;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1044 let mut divergence = Divergence {
1045 head: head.hash.clone(),
1046 base: base.hash.clone(),
1047 merge_base: merge_base.clone(),
1048 behind,
1049 ..Divergence::default()
1050 };
1051 let merge_base_tree = match &merge_base {
1052 Some(hash) => target_history
1053 .iter()
1054 .find(|commit| commit.hash == *hash)
1055 .map(|commit| commit.tree_hash.clone()),
1056 None => None,
1057 };
1058 let Some(merge_base_tree) = merge_base_tree else {
1059 // No common history to compare from: say nothing is known.
1060 divergence.truncated = true;
1061 return Ok(Some(divergence));
1062 };
1063 let (ours, truncated_ours) =
1064 diff::changed_paths(&source_git, Some(&merge_base_tree), &head.tree_hash).await?;
1065 divergence.ours = ours;
1066 divergence.truncated = truncated_ours;
1067 if behind {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1068 let now = now_ms();
1069 let key = (target.id.clone(), merge_base_tree.clone(), base.tree_hash.clone());
1070 let (theirs, truncated_theirs) = match THEIRS.with(|memo| memo.borrow().get(&key, now)) {
1071 Some(kept) => kept,
1072 None => {
1073 let found = diff::changed_paths(&target_git, Some(&merge_base_tree), &base.tree_hash).await?;
1074 THEIRS.with(|memo| memo.borrow_mut().put(key, found.clone(), now));
1075 found
1076 }
1077 };
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1078 divergence.theirs = theirs;
1079 divergence.truncated |= truncated_theirs;
1080 }
1081 Ok(Some(divergence))
1082 }
1083
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1084 /// A target branch's history from its head, worked out once per head
1085 /// for every pull request asking about it (coalesce.rs). The head is
1086 /// read under the refs version; the history by its hash, which the
1087 /// object cache keeps for good.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1088 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 daily1089 let now = now_ms();
1090 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 bar1091 .map(|version| (target.id.clone(), branch.to_owned(), version));
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1092 if let Some(key) = &key
1093 && let Some(side) = TARGETS.with(|memo| memo.borrow().get(key, now))
1094 {
1095 return Ok(side);
1096 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1097 let history = match git.log(branch, 1).await?.first() {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1098 Some(head) => git.log(&head.hash, MAX_ANCESTRY).await?,
1099 None => Vec::new(),
1100 };
1101 let side = coalesce::TargetSide::new(history);
1102 if let Some(key) = key {
1103 TARGETS.with(|memo| memo.borrow_mut().put(key, side.clone(), now));
1104 }
1105 Ok(side)
1106 }
1107
Pull requests from branches1108 async fn head(&self, a: HeadArgs) -> Result<Option<String>> {
1109 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
1110 return Ok(None);
1111 };
Workflows run when an agent's pull request is marked ready1112 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 daily1113 let git = self.read_git(&repo).await?;
Pull requests from branches1114 Ok(git
Workflows run when an agent's pull request is marked ready1115 .log(branch, 1)
Pull requests from branches1116 .await?
1117 .into_iter()
1118 .next()
1119 .map(|commit| commit.hash))
1120 }
1121
Merge queue: tested states are deleted once their entry leaves1122 async fn delete_branch(&self, a: DeleteBranchArgs) -> Result<Outcome<bool>> {
1123 if !a.branch.starts_with(G1T_BRANCH_PREFIX) {
1124 return Ok(Outcome::fail(
1125 FailureCode::Forbidden,
1126 "Only branches g1t made for itself can be deleted this way.",
1127 ));
1128 }
1129 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
1130 return Ok(not_found());
1131 };
Merge branch 'worktree-agent-a2013627e5ea4ab13'1132 let repo = match self.unpaused(repo).await? {
1133 Ok(repo) => repo,
1134 Err((code, message)) => return Ok(Outcome::fail(code, message)),
1135 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1136 self.live(&repo).await?;
Merge queue: tested states are deleted once their entry leaves1137 let git = self.store.open(&store_key(&repo)).await?;
1138 let Some(old) = git
1139 .branches()
1140 .await?
1141 .into_iter()
1142 .find(|branch| branch.name == a.branch)
1143 .map(|branch| branch.hash)
1144 else {
1145 return Ok(Outcome::Ok(false));
1146 };
1147 let access = git.access(Scope::Write).await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1148 let deleted = land::delete_ref(&access, &a.branch, &old).await?;
1149 self.refs_moved(&repo.id).await;
1150 if let Err(reason) = deleted {
Merge queue: tested states are deleted once their entry leaves1151 return Ok(Outcome::fail(
1152 FailureCode::Conflict,
1153 format!("{} could not be deleted: {reason}", a.branch),
1154 ));
1155 }
1156 Ok(Outcome::Ok(true))
1157 }
1158
Issues and pull requests replace intents and attempts1159 async fn fork_for_pull(&self, a: ForkArgs) -> Result<Outcome<Repo>> {
Rust repos service with shipping; pull requests kept in the model1160 let viewer = Some(a.actor.clone());
1161 let Some(source) = self
1162 .registry
1163 .by_id(&a.source_id)
1164 .await?
1165 .filter(|repo| can_read(repo, &viewer))
1166 else {
1167 return Ok(not_found());
1168 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1169 if let Some((code, message)) = lifecycle::archived_refusal(&source) {
1170 return Ok(Outcome::fail(code, message));
1171 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'1172 // Its working copy is made in its namespace: not while it moves.
1173 let source = match self.unpaused(source).await? {
1174 Ok(source) => source,
1175 Err((code, message)) => return Ok(Outcome::fail(code, message)),
1176 };
Rust repos service with shipping; pull requests kept in the model1177 let now = now_ms();
1178 let fork = Repo {
1179 id: new_id("rep", now),
Issues and pull requests replace intents and attempts1180 namespace: PULLS_NAMESPACE.to_owned(),
1181 name: a.pull_id.clone(),
Rust repos service with shipping; pull requests kept in the model1182 description: None,
1183 // A fork is exactly as visible as the repo it came from.
1184 is_private: source.is_private,
1185 owner_id: a.actor.id.clone(),
1186 default_branch: source.default_branch.clone(),
1187 fork_of: Some(source.id.clone()),
Agents as a team: lifecycle, merge queue, billing and a new shell1188 protected: false,
RFC 3339 timestamps in identity and repos1189 created_at: rfc3339(now),
Search across all of g1t, Explore, and a command palette1190 topics: Vec::new(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1191 website: None,
1192 archived_at: None,
Rust repos service with shipping; pull requests kept in the model1193 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1194 // Artifacts forks within a namespace: the copy goes where its
1195 // repository is.
1196 let (namespace, _) = store::locate(&store_key(&source));
1197 self.registry
1198 .claim_store_key(&fork, Some(&namespace), &self.store.default_namespace())
1199 .await?;
Rust repos service with shipping; pull requests kept in the model1200 self.store
1201 .open(&store_key(&source))
1202 .await?
1203 .fork(&store_key(&fork))
1204 .await?;
1205 self.registry.insert(&fork).await?;
1206 self.publish(NewEvent {
1207 kind: "repo.forked",
1208 source: SOURCE,
1209 repo_id: Some(source.id.clone()),
1210 actor: Some(a.actor.id),
1211 data: RepoForked {
1212 repo_id: fork.id.clone(),
1213 source_repo_id: source.id,
Issues and pull requests replace intents and attempts1214 pull_id: a.pull_id,
Rust repos service with shipping; pull requests kept in the model1215 },
1216 })
1217 .await?;
1218 Ok(Outcome::Ok(fork))
1219 }
1220
1221 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 200ms1222 let found = self.registry.by_path(&a.path).await?;
1223 Ok(match self.authorize_git(&a.path, &a.viewer, a.service, found).await? {
1224 Outcome::Ok(repo) => {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1225 self.live(&repo).await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1226 let write = a.service == GitService::ReceivePack;
1227 if write {
1228 // A push with this credential would not pass through
1229 // here, so nothing that lists the refs is kept until it
1230 // has expired (see refs_cache.rs).
1231 let until = now_ms() + store::CREDENTIAL_LIFE_MS + 60_000;
1232 if let Err(error) = self.registry.refs_open(&repo.id, until).await {
1233 // Before the column exists nothing is kept anyway.
1234 if registry::refs_state(&repo.id).is_some() {
1235 return Err(error);
1236 }
1237 }
1238 }
1239 let scope = if write { Scope::Write } else { Scope::Read };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1240 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 200ms1241 }
1242 Outcome::Fail(failure) => Outcome::Fail(failure),
1243 })
1244 }
1245
1246 /// The repository at `path` (`found`, as just read), if the viewer may
1247 /// use `service` on it: fetch from it, or push to it. A push to a path
1248 /// with nothing there makes the repository, in a workspace the pusher
1249 /// belongs to.
1250 async fn authorize_git(
1251 &self,
1252 path: &RepoPath,
1253 viewer: &Viewer,
1254 service: GitService,
1255 found: Option<Repo>,
1256 ) -> Result<Outcome<Repo>> {
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1257 let mut a = GitAccessArgs {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1258 path: path.clone(),
1259 viewer: viewer.clone(),
1260 service,
1261 };
Rust repos service with shipping; pull requests kept in the model1262 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 step1263 // An access token: pushing needs code:write, reading a private
1264 // repository code:read. A public repository reads as it would for
1265 // anyone. Which repositories a token reaches is its owner's, checked
1266 // below as for anyone.
1267 if let Some(access) = a.viewer.as_ref().and_then(|user| user.token.as_deref()).cloned() {
Merge branch 'worktree-agent-a3abfcce648e87dca'1268 // A workflow job's token reaches its own repository only, and
1269 // the working copies of that repository's pull requests, where
1270 // their heads are.
1271 if let Some(refused) = g1t_contracts::scopes::decide_repo(&access, &format!("{}/{}", path.namespace, path.name)) {
1272 let source = match found.as_ref().and_then(|repo| repo.fork_of.as_deref()) {
1273 Some(source_id) => self.registry.by_id(source_id).await?,
1274 None => None,
1275 };
1276 if !source.is_some_and(|source| access.reaches(&format!("{}/{}", source.namespace, source.name))) {
1277 return Ok(Outcome::fail(
1278 FailureCode::Forbidden,
1279 format!("{}\n", refused.reason.unwrap_or_default()),
1280 ));
1281 }
1282 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1283 let public = found.as_ref().is_some_and(|repo| !repo.is_private);
1284 let decision = g1t_contracts::scopes::decide_git(&access, write, public);
1285 if !decision.allowed {
1286 return Ok(Outcome::fail(
1287 FailureCode::Forbidden,
1288 format!("{}\n", decision.reason.unwrap_or_default()),
1289 ));
1290 }
1291 if !write && !access.allows(g1t_contracts::scopes::Scope::CodeRead) {
1292 a.viewer = None;
1293 }
1294 }
1295
Rust repos service with shipping; pull requests kept in the model1296 // Anonymous callers are asked to authenticate whether or not the repo
1297 // exists, so private repos cannot be told apart from missing ones.
1298 let denied = || match &a.viewer {
1299 Some(_) => not_found(),
1300 None => Outcome::fail(FailureCode::Unauthenticated, "Authentication required."),
1301 };
Agents as a team: lifecycle, merge queue, billing and a new shell1302 // An agent's token works through the API only: its sandbox has its
1303 // own way to push, to its own pull request.
1304 if a.viewer.as_ref().is_some_and(|user| user.kind == PrincipalKind::Agent) {
1305 return Ok(Outcome::fail(
1306 FailureCode::Forbidden,
1307 "A g1t agent's token cannot be used with git.",
1308 ));
1309 }
Rust repos service with shipping; pull requests kept in the model1310 if let (true, Some(user)) = (write, &a.viewer)
1311 && !user.verified
1312 {
1313 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
1314 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1315 let repo = match found {
Rust repos service with shipping; pull requests kept in the model1316 Some(repo) => {
1317 let allowed = if write {
1318 can_write(&repo, &a.viewer)
1319 } else {
Members can read a private repository's pull request forks1320 self.may_read(&repo, &a.viewer).await?
Rust repos service with shipping; pull requests kept in the model1321 };
1322 if !allowed {
1323 return Ok(denied());
1324 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1325 // An archived repository, or a pull request's copy of one,
1326 // is read-only.
1327 if write {
1328 let archived = match &repo.fork_of {
1329 Some(source) => self.registry.by_id(source).await?,
1330 None => Some(repo.clone()),
1331 };
1332 match archived {
1333 Some(source) => {
1334 if let Some((code, message)) = lifecycle::archived_refusal(&source) {
1335 return Ok(Outcome::fail(code, format!("{message}\n")));
1336 }
1337 }
1338 // The repository it was copied from is deleted.
1339 None => return Ok(denied()),
1340 }
1341 }
Rust repos service with shipping; pull requests kept in the model1342 repo
1343 }
1344 None => {
Workspaces own repositories1345 // Push to create, in a workspace the pusher belongs to.
Rust repos service with shipping; pull requests kept in the model1346 let owner = a
1347 .viewer
1348 .as_ref()
Workspaces own repositories1349 .filter(|user| write && user.is_member(&a.path.namespace.to_lowercase()));
Rust repos service with shipping; pull requests kept in the model1350 let Some(owner) = owner else {
1351 return Ok(denied());
1352 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1353 let created = self.create(push_to_create(owner, &a.path)).await?;
Rust repos service with shipping; pull requests kept in the model1354 match created {
1355 Outcome::Ok(repo) => repo,
1356 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
1357 }
1358 }
1359 };
Merge branch 'worktree-agent-a2013627e5ea4ab13'1360 // A push, or a credential to push with, waits while the repository
1361 // moves between namespaces (moves.rs), and goes to where it is now.
1362 if write {
1363 return Ok(match self.unpaused(repo).await? {
1364 Ok(repo) => Outcome::Ok(repo),
1365 Err((code, message)) => Outcome::fail(code, format!("{message}\n")),
1366 });
1367 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1368 Ok(Outcome::Ok(repo))
Rust repos service with shipping; pull requests kept in the model1369 }
1370
1371 async fn land(&self, a: LandArgs) -> Result<Outcome<Landed>> {
1372 let actor: Viewer = Some(a.actor.clone());
Pull requests from branches1373 let Some(source) = self.registry.by_id(&a.source_id).await? else {
Rust repos service with shipping; pull requests kept in the model1374 return Ok(not_found());
1375 };
Pull requests from branches1376 // A fork lands on the repository it came from; a branch on its own.
1377 let target = match &source.fork_of {
Rust repos service with shipping; pull requests kept in the model1378 Some(id) => self.registry.by_id(id).await?,
Pull requests from branches1379 None => Some(source.clone()),
Rust repos service with shipping; pull requests kept in the model1380 };
1381 let Some(target) = target.filter(|repo| can_read(repo, &actor)) else {
1382 return Ok(not_found());
1383 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1384 if !registry::can(&target, &actor, Capability::Merge) {
Rust repos service with shipping; pull requests kept in the model1385 return Ok(Outcome::fail(
1386 FailureCode::Forbidden,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1387 access::needs(Capability::Merge, &format!("{}/{}", target.namespace, target.name)),
Rust repos service with shipping; pull requests kept in the model1388 ));
1389 }
1390 if !a.actor.verified {
1391 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
1392 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1393 if let Some((code, message)) = lifecycle::archived_refusal(&target) {
1394 return Ok(Outcome::fail(code, message));
1395 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'1396 // Moving between namespaces: wait for it (moves.rs). Both are read
1397 // again once it is done, for their new keys.
1398 let (source, target) = match (self.unpaused(source).await?, self.unpaused(target).await?) {
1399 (Ok(source), Ok(target)) => (source, target),
1400 (Err((code, message)), _) | (_, Err((code, message))) => return Ok(Outcome::fail(code, message)),
1401 };
Rust repos service with shipping; pull requests kept in the model1402
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1403 let branch = &a.target_branch.clone().unwrap_or_else(|| target.default_branch.clone());
Pull requests from branches1404 let from_fork = source.id != target.id;
1405 let source_branch = match a.branch {
1406 Some(name) if !from_fork && name == *branch => {
1407 return Ok(Outcome::fail(
1408 FailureCode::Invalid,
1409 format!("{branch} cannot be merged into itself."),
1410 ));
1411 }
1412 Some(name) => name,
1413 None if from_fork => branch.clone(),
1414 None => {
1415 return Ok(Outcome::fail(
1416 FailureCode::Invalid,
1417 "Say which branch to merge.",
1418 ));
1419 }
1420 };
1421
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1422 self.live(&source).await?;
Pull requests from branches1423 let source_git = self.store.open(&store_key(&source)).await?;
Rust repos service with shipping; pull requests kept in the model1424 let target_git = self.store.open(&store_key(&target)).await?;
Pull requests from branches1425 let history = source_git.log(&source_branch, MAX_ANCESTRY).await?;
Rust repos service with shipping; pull requests kept in the model1426 let Some(new) = history.first().map(|commit| commit.hash.clone()) else {
1427 return Ok(Outcome::fail(
1428 FailureCode::Conflict,
Issues and pull requests replace intents and attempts1429 "This pull request has no commits to merge.",
Rust repos service with shipping; pull requests kept in the model1430 ));
1431 };
1432 let old = target_git
1433 .log(branch, 1)
1434 .await?
1435 .into_iter()
1436 .next()
1437 .map(|commit| commit.hash);
1438
1439 if old.as_deref() == Some(new.as_str()) {
Diffs on attempts; hosted agent presented as the g1t agent1440 return Ok(Outcome::Ok(Landed {
1441 commit: new,
1442 previous: None,
1443 }));
Rust repos service with shipping; pull requests kept in the model1444 }
1445 // Moving the branch to a commit that does not descend from its
1446 // current head would discard whatever landed in between.
1447 if let Some(old) = &old
Pull requests from branches1448 && !descends_from(&source_git, &history, old).await?
Rust repos service with shipping; pull requests kept in the model1449 {
Pull requests from branches1450 let remedy = if from_fork {
1451 format!("Pull {branch} into the pull request's fork, push, and merge again.")
1452 } else {
1453 format!("Merge {branch} into {source_branch}, push, and merge again.")
1454 };
Rust repos service with shipping; pull requests kept in the model1455 return Ok(Outcome::fail(
1456 FailureCode::Conflict,
Pull requests from branches1457 format!("{branch} has moved since this pull request was opened. {remedy}"),
Rust repos service with shipping; pull requests kept in the model1458 ));
1459 }
1460
Pull requests from branches1461 // For a branch the objects are already in the target; sending them
1462 // again is harmless and keeps one way of moving a ref.
1463 let source_access = source_git.access(Scope::Read).await?;
Rust repos service with shipping; pull requests kept in the model1464 let target_access = target_git.access(Scope::Write).await?;
1465 let pushed =
1466 land::fast_forward(&source_access, &target_access, branch, old.as_deref(), &new)
1467 .await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1468 self.refs_moved(&target.id).await;
Rust repos service with shipping; pull requests kept in the model1469 if let Err(reason) = pushed {
Issues and pull requests replace intents and attempts1470 // Most often another pull request landed between the check and the push.
Rust repos service with shipping; pull requests kept in the model1471 return Ok(Outcome::fail(
1472 FailureCode::Conflict,
1473 format!("{branch} could not be updated: {reason}"),
1474 ));
1475 }
GitHub Actions on g1t, part one: reading workflows1476 self.publish_push(
1477 &target,
1478 &format!("refs/heads/{branch}"),
1479 old.as_deref(),
1480 &new,
Merge branch 'worktree-agent-a3abfcce648e87dca'1481 Some(&a.actor),
GitHub Actions on g1t, part one: reading workflows1482 )
Events service in Rust, with RFC 3339 times and accurate push events1483 .await?;
Diffs on attempts; hosted agent presented as the g1t agent1484 Ok(Outcome::Ok(Landed {
1485 commit: new,
1486 previous: old,
1487 }))
1488 }
1489
1490 async fn compare(&self, a: CompareArgs) -> Result<Outcome<Comparison>> {
1491 let Some(repo) = self
Members can read a private repository's pull request forks1492 .visible(self.registry.by_id(&a.repo_id).await?, &a.viewer)
Diffs on attempts; hosted agent presented as the g1t agent1493 .await?
1494 else {
1495 return Ok(not_found());
1496 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1497 let git = self.read_git(&repo).await?;
Pull requests from branches1498 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 bar1499 // A pull request into another branch is compared from where it
1500 // left that branch.
1501 let base_branch = a.base_branch.clone();
Agents as a team: lifecycle, merge queue, billing and a new shell1502 // The head's history is only searched when the base is worked out
1503 // from another branch.
1504 let depth = if a.base.is_some() || is_commit_hash(head_ref) { 1 } else { MAX_ANCESTRY };
1505 let history = git.log(head_ref, depth).await?;
Diffs on attempts; hosted agent presented as the g1t agent1506 let Some(head) = history.first() else {
1507 return Ok(Outcome::fail(
1508 FailureCode::Conflict,
Pull requests from branches1509 "There are no commits to compare.",
Diffs on attempts; hosted agent presented as the g1t agent1510 ));
1511 };
1512
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1513 // Where the head's history meets the default branch of `against`,
1514 // or the branch asked for.
Pull requests from branches1515 let shared_with = async |against: &Repo| -> Result<Option<String>> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1516 let against_git = self.read_git(against).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1517 let branch = base_branch.as_deref().unwrap_or(&against.default_branch);
Pull requests from branches1518 let shared: HashSet<String> = against_git
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1519 .log(branch, MAX_ANCESTRY)
Pull requests from branches1520 .await?
1521 .into_iter()
1522 .map(|commit| commit.hash)
1523 .collect();
1524 nearest_ancestor_in(&git, &history, &shared).await
1525 };
Diffs on attempts; hosted agent presented as the g1t agent1526 let base = match (a.base, &repo.fork_of) {
1527 (Some(base), _) => Some(base),
1528 // A fork is compared with the last commit it shares with the
1529 // repository it came from.
1530 (None, Some(target_id)) => match self.registry.by_id(target_id).await? {
Pull requests from branches1531 Some(target) => shared_with(&target).await?,
Diffs on attempts; hosted agent presented as the g1t agent1532 None => None,
1533 },
Pull requests from branches1534 // A branch, with the point where it left the default branch.
Agents as a team: lifecycle, merge queue, billing and a new shell1535 // A single commit, with its first parent.
1536 (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 bar1537 (None, None) if head_ref != base_branch.as_deref().unwrap_or(&repo.default_branch) => {
1538 shared_with(&repo).await?
1539 }
Diffs on attempts; hosted agent presented as the g1t agent1540 (None, None) => head.parents.first().cloned(),
1541 };
1542 let base_tree = match &base {
1543 Some(base) => git
1544 .log(base, 1)
1545 .await?
1546 .into_iter()
1547 .next()
1548 .map(|commit| commit.tree_hash),
1549 None => None,
1550 };
1551 let (files, truncated) =
1552 diff::compare_trees(&git, base_tree.as_deref(), &head.tree_hash).await?;
1553 Ok(Outcome::Ok(Comparison {
1554 base,
1555 head: head.hash.clone(),
1556 files,
1557 truncated,
1558 }))
Rust repos service with shipping; pull requests kept in the model1559 }
1560
Merge branch 'worktree-agent-a3abfcce648e87dca'1561 /// Reports that `git_ref` of `repo` (a full ref) now points to `after`,
1562 /// moved by `actor` (marked when that was a workflow job's token).
Events service in Rust, with RFC 3339 times and accurate push events1563 async fn publish_push(
1564 &self,
1565 repo: &Repo,
GitHub Actions on g1t, part one: reading workflows1566 git_ref: &str,
1567 before: Option<&str>,
Events service in Rust, with RFC 3339 times and accurate push events1568 after: &str,
Merge branch 'worktree-agent-a3abfcce648e87dca'1569 actor: Option<&User>,
Events service in Rust, with RFC 3339 times and accurate push events1570 ) -> Result<()> {
Merge branch 'worktree-agent-a3abfcce648e87dca'1571 let caused_by_job = actor.and_then(g1t_contracts::events::job_run_of).map(str::to_owned);
1572 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 daily1573 }
1574
1575 /// `publish_push`, saying whether the push reached the store without
1576 /// being scanned for secrets first.
Merge branch 'worktree-agent-a3abfcce648e87dca'1577 #[allow(clippy::too_many_arguments)]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1578 async fn publish_git_push(
1579 &self,
1580 repo: &Repo,
1581 git_ref: &str,
1582 before: Option<&str>,
1583 after: &str,
1584 actor: Option<String>,
1585 unscanned: bool,
Merge branch 'worktree-agent-a3abfcce648e87dca'1586 caused_by_job: Option<String>,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1587 ) -> Result<()> {
Rust repos service with shipping; pull requests kept in the model1588 self.publish(NewEvent {
1589 kind: "git.push",
1590 source: SOURCE,
1591 repo_id: Some(repo.id.clone()),
1592 actor,
1593 data: GitPush {
1594 repo_id: repo.id.clone(),
GitHub Actions on g1t, part one: reading workflows1595 git_ref: git_ref.to_owned(),
1596 before: before.map(str::to_owned),
Rust repos service with shipping; pull requests kept in the model1597 after: after.to_owned(),
GitHub Actions on g1t, part one: reading workflows1598 default_branch: git_ref.strip_prefix("refs/heads/")
1599 == Some(repo.default_branch.as_str()),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1600 unscanned,
Merge branch 'worktree-agent-a3abfcce648e87dca'1601 caused_by_job,
Rust repos service with shipping; pull requests kept in the model1602 },
1603 })
1604 .await
1605 }
1606
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1607 /// Git over HTTPS. Only what decides the answer happens before it:
1608 /// the repository, who is asking and whether they may, the free
1609 /// workspace limits, push protection, and the store's own answer. The
1610 /// audit entry and what a push changed are recorded once git has its
1611 /// answer. Each answer says how long its steps took (`Server-Timing`).
1612 async fn git_http(&self, request: Request, env: &Env, ctx: &Context) -> Result<Response> {
1613 let mut timing = git_http::Timing::start();
Rust repos service with shipping; pull requests kept in the model1614 let Some(git) = git_http::parse(&request.url()?) else {
1615 return Response::error("Not found", 404);
1616 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1617 let response = match self.answer_git(request, &git, env, ctx, &mut timing).await {
1618 Ok(response) => response,
1619 // The git store is busy: git hears when to try again.
1620 Err(error) => match resilience::busy(&error.to_string()) {
1621 Some(busy) => git_http::busy_response(busy)?,
1622 None => return Err(error),
1623 },
1624 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1625 timing.apply(response)
1626 }
1627
1628 async fn answer_git(
1629 &self,
1630 request: Request,
1631 git: &git_http::GitRequest,
1632 env: &Env,
1633 ctx: &Context,
1634 timing: &mut git_http::Timing,
1635 ) -> Result<Response> {
1636 let write = git.service == GitService::ReceivePack;
1637 let get = request.method() == Method::Get;
1638 let identity = env.service("IDENTITY")?;
1639 // The repository and the caller's credentials, at once. A fetch may
1640 // go by the row as read a moment ago, for the same clone's next
1641 // request; a push always reads it. Anonymous callers cost nothing.
1642 let lookup = async {
1643 if write {
1644 self.registry.by_path(&git.path).await
1645 } else {
1646 self.registry.by_path_recent(&git.path).await
1647 }
1648 };
1649 let (found, viewer) =
1650 futures_util::future::join(lookup, git_http::viewer(&request, &identity)).await;
Merge branch 'worktree-agent-a8385d293d42c913a'1651 let mut found = found?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1652 timing.mark("repo");
Merge branch 'worktree-agent-a8385d293d42c913a'1653 // A workspace alias staff set (identity's aliases.rs: `g1t` for
1654 // `flagon-io`) is answered in place, as the repository under the
1655 // workspace's slug: pushes and some clients do not follow
1656 // redirects. Everything after this sees only the workspace's slug.
1657 let aliased = match found {
1658 Some(_) => None,
1659 None => git_http::aliased(git, &identity).await?,
1660 };
1661 if let Some(aliased) = &aliased {
1662 found = if write {
1663 self.registry.by_path(&aliased.path).await?
1664 } else {
1665 self.registry.by_path_recent(&aliased.path).await?
1666 };
1667 timing.mark("alias");
1668 }
1669 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 200ms1670 if found.is_none() {
1671 // A workspace that was renamed: git follows a redirect when it
1672 // first asks for refs, and uses the new address from then on.
1673 // A repository transferred to another workspace: the same, to
1674 // its new path. Fetches and pushes both follow either.
1675 let url = request.url()?;
1676 let (renamed, moved) = futures_util::future::join(
1677 git_http::renamed(&url, &identity),
1678 self.registry.resolve_moved(&git.path),
1679 )
1680 .await;
1681 timing.mark("moved");
1682 if let Some(location) = renamed? {
1683 return git_http::moved(&location, get);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1684 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1685 if let Some(now) = moved?
1686 && let Some(location) = git_http::transferred(&url, &now)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1687 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1688 return git_http::moved(&location, get);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1689 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1690 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1691 let viewer = viewer?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1692 // A run credential is checked against its grants, then acts as the
1693 // 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 200ms1694 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 API1695 run_access::Admitted::Go { request, viewer, entry } => (request, viewer, entry),
1696 run_access::Admitted::Refused(response) => return Ok(response),
1697 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1698 let mut after = AfterGit {
1699 audit,
1700 status: 0,
1701 message: None,
1702 push: None,
1703 };
1704 let repo = match self.authorize_git(&git.path, &viewer, git.service, found).await? {
1705 Outcome::Ok(repo) => repo,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1706 refused => {
1707 let response = git_http::refuse(refused)?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1708 after.ended(response.status_code(), None);
1709 after.spawn(env, ctx);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1710 return Ok(response);
1711 }
Rust repos service with shipping; pull requests kept in the model1712 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1713 // A pull request's working copy removed after it closed is made
1714 // again before git uses it (forks.rs).
1715 self.live(&repo).await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1716 timing.mark("access");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1717 // Clones check out the default branch g1t keeps, which can have
1718 // changed since the store made the repository.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1719 let default_branch = repo.fork_of.is_none().then(|| repo.default_branch.clone());
1720 let key = store_key(&repo);
1721 let scope = if write { Scope::Write } else { Scope::Read };
1722 let mut request = request;
1723 let protocol = refs_cache::protocol(request.headers().get("git-protocol")?.as_deref());
1724 // A fetch's POST is read here, to tell an `ls-refs` from a fetch of
1725 // objects; the store would have it read in full anyway.
1726 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 daily1727 // What it asks the store, for the meters (meters.rs).
1728 let call = git_ops::classify(git.service, git.endpoint, get, body.as_deref());
Merge branch 'worktree-agent-a2013627e5ea4ab13'1729 // Answers kept from the usual store may name refs the fallback
1730 // store does not have (fallback.rs): none are used, or kept.
1731 let fallback = self.store.on_fallback(&key);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1732 // An answer that lists refs may have been kept: see refs_cache.rs.
1733 let kept_key = refs_cache::kind(git, get, protocol, body.as_deref())
Merge branch 'worktree-agent-a2013627e5ea4ab13'1734 .filter(|_| !fallback)
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1735 .zip(refs_cache::usable(registry::refs_state(&repo.id), now_ms()))
1736 .map(|(kind, version)| {
1737 refs_cache::Key::new(&repo.id, version, default_branch.as_deref(), protocol, &kind)
1738 });
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1739 // A fresh clone's pack may have been kept too: see pack_cache.rs.
1740 // Under the same refs version, so never across a change to them.
1741 let pack_key = self
1742 .packs
1743 .as_ref()
Merge branch 'worktree-agent-a2013627e5ea4ab13'1744 .filter(|_| !fallback)
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1745 .and_then(|_| {
1746 let encoding = request.headers().get("content-encoding").ok().flatten();
1747 pack_cache::cacheable(git, get, protocol, encoding.as_deref(), body.as_deref())
1748 })
1749 .zip(refs_cache::usable(registry::refs_state(&repo.id), now_ms()))
1750 .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 200ms1751 // A kept answer and the free workspace limits, with a kept
1752 // credential looked up alongside. A kept answer goes back without
1753 // waiting for the credential, which it does not need.
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1754 let ((answer, pack, limited), kept_access) = {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1755 let shared = self.shared.as_deref();
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1756 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 200ms1757 async {
1758 match &kept_key {
1759 Some(kept_key) => refs_cache::get(shared, kept_key).await,
1760 None => None,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1761 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1762 },
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1763 async {
1764 match (&pack_key, self.packs.as_deref()) {
1765 (Some(pack_key), Some(packs)) => pack_cache::get(packs, pack_key).await,
1766 _ => None,
1767 }
1768 },
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1769 self.git_limits(call, git, &repo, env),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1770 ));
1771 let kept_access = std::pin::pin!(self.store.kept_access(&key, scope));
1772 match futures_util::future::select(answer_and_limits, kept_access).await {
1773 futures_util::future::Either::Left((first, kept_access)) => {
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1774 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 200ms1775 (first, if answered { None } else { kept_access.await })
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1776 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1777 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 look1778 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1779 };
1780 timing.mark("kept");
A clone answered from the pack cache is served before the free operation cap: it is not an operation1781 // A kept pack first: it never reaches the store, so it is never an
1782 // operation, and a free workspace past its operation cap still gets
1783 // it. (The other limits are a push's, and a pack is only a fetch.)
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1784 if let Some(kept) = pack {
1785 timing.note("pack", "hit");
1786 let sent = body.as_ref().map_or(0, |body| body.len() as u64);
1787 meters::record(pack_cache::HIT, &key, sent, kept.size);
1788 after.ended(200, None);
1789 after.spawn(env, ctx);
1790 return kept.response();
1791 }
A clone answered from the pack cache is served before the free operation cap: it is not an operation1792 if let Some((response, status, message)) = limited? {
1793 after.ended(status, Some(message.to_owned()));
1794 after.spawn(env, ctx);
1795 return Ok(response);
1796 }
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1797 if pack_key.is_some() {
1798 timing.note("pack", "miss");
1799 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1800 if let (Some((entry, found)), Some(kept_key)) = (answer, &kept_key) {
1801 timing.note("refs", found.as_str());
1802 if found == refs_cache::Found::Shared {
1803 let (kept_key, entry) = (kept_key.clone(), entry.clone());
1804 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 look1805 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1806 // Never reached the store: never an operation.
1807 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 200ms1808 after.ended(200, None);
1809 after.spawn(env, ctx);
1810 return entry.response();
1811 }
1812 if kept_key.is_some() {
1813 timing.note("refs", "miss");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1814 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1815 // The store's credential: one made a moment ago, here or in another
1816 // isolate (see store.rs), or a new one.
1817 let access = match kept_access {
1818 Some((access, from)) => {
1819 timing.note("cred", from.as_str());
1820 access
1821 }
1822 None => {
1823 let access = self.store.mint_access(&key, scope).await?;
1824 timing.mark("mint");
1825 timing.note("cred", "mint");
1826 access
1827 }
1828 };
1829 // Should the store turn a kept credential down, a fetch's first
1830 // request is tried again with a new one; the requests after it then
1831 // have that one too.
1832 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 API1833 // 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 200ms1834 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 merge1835 // Rulesets: what the rules of the branches and tags it changes
1836 // refuse is declined, saying which rule and why (rules.rs).
1837 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 daily1838 // What a push may bring (pack_limits.rs): the repository's size is
1839 // its own and its pull requests' working copies'.
1840 let limits = if write && !get {
1841 git_http::PushLimits {
1842 held: self.held(&repo).await,
1843 repo_limit: self.repo_limit,
1844 large: self.large_pushes,
1845 ..git_http::PushLimits::default()
1846 }
1847 } else {
1848 git_http::PushLimits::default()
1849 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1850 let mut outcome = git_http::forward(
1851 request,
1852 body,
1853 git,
1854 &access,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1855 rules,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1856 default_branch.as_deref(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1857 limits,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1858 scan,
1859 )
1860 .await?;
1861 let turned_down = matches!(
1862 &outcome,
1863 git_http::Push::Forwarded(forwarded) if matches!(forwarded.response.status_code(), 401 | 403)
1864 );
1865 if turned_down {
1866 self.store.forget_access(&key).await;
1867 if let Some(again) = again {
1868 let access = self.store.mint_access(&key, scope).await?;
1869 let nothing = async |_: &[u8]| Ok(None);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1870 outcome = git_http::forward(
1871 again,
1872 None,
1873 git,
1874 &access,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1875 async |_: &[u8], _: bool| Ok(None),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1876 default_branch.as_deref(),
1877 git_http::PushLimits::default(),
1878 nothing,
1879 )
1880 .await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1881 }
1882 }
Agents as a team: lifecycle, merge queue, billing and a new shell1883 let forwarded =
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1884 match outcome {
Agents as a team: lifecycle, merge queue, billing and a new shell1885 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 API1886 git_http::Push::Refused(response) => {
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1887 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 200ms1888 after.spawn(env, ctx);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1889 return Ok(response);
1890 }
1891 git_http::Push::Blocked(response) => {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1892 after.ended(403, Some("The push adds a secret.".to_owned()));
1893 after.spawn(env, ctx);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1894 return Ok(response);
1895 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1896 git_http::Push::Declined(response, reason) => {
1897 after.ended(403, Some(format!("The push was declined: {reason}.")));
1898 after.spawn(env, ctx);
1899 return Ok(response);
1900 }
Agents as a team: lifecycle, merge queue, billing and a new shell1901 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1902 if forwarded.from_store {
1903 let received = forwarded
1904 .response
1905 .headers()
1906 .get("content-length")?
1907 .and_then(|length| length.parse().ok())
1908 .unwrap_or(0);
1909 meters::record(call.meter(), &key, forwarded.sent, received);
1910 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1911 timing.mark("store");
1912 let mut response = forwarded.response;
1913 let status = response.status_code();
1914 if write && !get {
1915 // A push: the store has moved its refs once it has answered in
1916 // full, so the answer is read before the change is recorded, and
1917 // only then goes back. Whoever fetches after it sees the push.
1918 let headers = response.headers().clone();
1919 headers.delete("content-length")?;
1920 let report = response.bytes().await?;
1921 self.refs_moved(&repo.id).await;
1922 timing.mark("refs");
1923 response = Response::from_bytes(report)?.with_headers(headers).with_status(status);
1924 } else if let (Some(kept_key), 200) = (&kept_key, status) {
1925 // A miss: this answer is kept for the next to ask.
1926 let headers = response.headers().clone();
1927 headers.delete("content-length")?;
1928 let body = response.bytes().await?;
1929 if let Some(content_type) = headers.get("content-type")? {
1930 let entry = refs_cache::Entry { content_type, body: body.clone() };
1931 if entry.keepable() {
1932 let shared = self.shared.clone();
1933 let kept_key = kept_key.clone();
1934 ctx.wait_until(async move { refs_cache::keep(shared.as_deref(), &kept_key, &entry).await });
1935 }
1936 }
1937 response = Response::from_bytes(body)?.with_headers(headers).with_status(status);
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1938 } else if let (Some(pack_key), Some(packs), true) = (&pack_key, &self.packs, forwarded.from_store) {
1939 // A fresh clone the bucket did not have: counted, and its pack
1940 // kept as it streams to git, when it is a whole one.
1941 meters::record(pack_cache::MISS, &key, forwarded.sent, 0);
1942 if status == 200 {
1943 let store_key = key.clone();
1944 let measured = Box::new(move |bytes: u64| meters::record_bytes(pack_cache::MISS, &store_key, 0, bytes));
1945 let (teed, filling) = pack_cache::tee(response, packs.clone(), pack_key, measured)?;
1946 response = teed;
1947 if let Some(filling) = filling {
1948 let pack_key = pack_key.clone();
1949 ctx.wait_until(async move {
1950 let filled = filling.await;
1951 if !matches!(filled, pack_cache::Filled::Kept { .. } | pack_cache::Filled::Abandoned) {
1952 worker::console_warn!("pack {} not kept: {filled:?}", pack_key.as_str());
1953 }
1954 });
1955 }
1956 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1957 }
1958 after.ended(status, None);
1959 if status == 200 && (forwarded.pack_bytes > 0 || !forwarded.pushed.is_empty()) {
1960 after.push = Some(PushDone {
1961 repo,
1962 pushed: forwarded.pushed,
1963 pack_bytes: forwarded.pack_bytes,
Merge branch 'worktree-agent-a3abfcce648e87dca'1964 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 200ms1965 actor: viewer.map(|user: User| user.id),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1966 unscanned: forwarded.unscanned,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1967 });
1968 }
1969 after.spawn(env, ctx);
1970 Ok(response)
1971 }
1972
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1973 /// The answer for a request a free workspace's limits stop, or a push
1974 /// to a full repository, with its status and reason for the audit log;
1975 /// `None` to go on.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1976 ///
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1977 /// A clone, fetch or push is a git operation, which the git store
1978 /// charges g1t for: counted for billing once the answer has gone back
1979 /// (meters.rs), and a free workspace far past its share is slowed down
1980 /// rather than charged (see git_ops.rs). Whether it is past it is
1981 /// decided from counts this isolate already holds: the database is not
1982 /// asked on the way. A free workspace is never charged for private
1983 /// storage: once its private repositories hold the free amount, pushes
1984 /// to them stop, checked when a push begins so that git shows the
1985 /// 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 200ms1986 async fn git_limits(
1987 &self,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1988 call: git_ops::GitCall,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1989 git: &git_http::GitRequest,
1990 repo: &Repo,
1991 env: &Env,
1992 ) -> Result<Option<(Response, u16, &'static str)>> {
1993 let namespace = git.path.namespace.to_lowercase();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1994 if meters::mapping_now().billable(call.meter()) > 0.0 {
1995 let now = now_ms();
1996 let hour = git_ops::hour_key(&rfc3339(now));
1997 let limits = git_ops::Limits::from_env(env);
1998 if let Some((month, hour_ops)) = git_ops::standing(&namespace, &hour, now)
1999 && git_ops::slow_down(month + 1, hour_ops + 1, limits.free_cap, limits.hourly)
2000 && git_ops::is_free_kept(env.service("BILLING").ok().as_ref(), &namespace).await
2001 {
2002 return Ok(Some((
2003 git_ops::too_many(&namespace, limits.free_cap, limits.hourly)?,
2004 429,
2005 "Too many git operations this hour.",
2006 )));
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2007 }
2008 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2009 if git.service == GitService::ReceivePack && git.endpoint == "info/refs" {
2010 let held = self.held(repo).await;
2011 if held >= self.repo_limit {
2012 let message = format!(
2013 "{}/{} 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",
2014 repo.namespace,
2015 repo.name,
2016 pack_limits::megabytes(held)
2017 );
2018 return Ok(Some((Response::error(message, 403)?, 403, "The repository is full.")));
2019 }
2020 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2021 if git.service == GitService::ReceivePack && git.endpoint == "info/refs" && repo.is_private {
2022 let free = git_ops::free_private_bytes(env);
2023 let held = self.registry.private_bytes(&namespace).await.unwrap_or(0);
2024 if git_ops::storage_full(held, free)
2025 && git_ops::is_free(env.service("BILLING").ok().as_ref(), &namespace).await
2026 {
2027 return Ok(Some((
2028 git_ops::storage_full_response(&namespace, held, free)?,
2029 403,
2030 "Free private storage is full.",
2031 )));
2032 }
2033 }
2034 Ok(None)
2035 }
Rust repos service with shipping; pull requests kept in the model2036
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2037 /// What a repository and its pull requests' working copies hold, as
2038 /// g1t counts it: read for a push's first request, kept a minute for
2039 /// the rest of it.
2040 async fn held(&self, repo: &Repo) -> u64 {
2041 let root = repo.fork_of.clone().unwrap_or_else(|| repo.id.clone());
2042 let now = now_ms();
2043 if let Some(held) = HELD.with(|held| held.borrow().get(&root, now)) {
2044 return held;
2045 }
2046 let held = self.registry.stored_bytes(&root).await.unwrap_or(0).max(0) as u64;
2047 HELD.with(|kept| kept.borrow_mut().put(root, held, now));
2048 held
2049 }
2050
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2051 /// What a push changed, recorded once git has its answer.
2052 async fn record_push(&self, push: PushDone) -> Result<()> {
2053 let PushDone {
2054 repo,
2055 pushed,
2056 pack_bytes,
2057 actor,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2058 unscanned,
Merge branch 'worktree-agent-a3abfcce648e87dca'2059 caused_by_job,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2060 } = push;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2061 // What the push stored, for billing's storage meter. A failure only
2062 // leaves the count short.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2063 if pack_bytes > 0
2064 && 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 put2065 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2066 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 put2067 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2068 if pushed.is_empty() {
2069 return Ok(());
2070 }
Rust repos service with shipping; pull requests kept in the model2071 // Artifacts' own push notifications are per repository, which does
Events service in Rust, with RFC 3339 times and accurate push events2072 // not fit a repo per pull request, so the front end reports pushes
2073 // 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 200ms2074 let stored = self.store.open(&store_key(&repo)).await?;
2075 for pushed in &pushed {
2076 // The store can refuse one ref and accept another, so each
2077 // branch is checked against where it actually is. A tag the
2078 // store cannot read back is taken as pushed.
2079 let moved = match pushed.branch() {
2080 Some(branch) => stored
2081 .log(branch, 1)
2082 .await?
2083 .first()
2084 .is_some_and(|commit| commit.hash == pushed.after),
2085 None => stored.log(&pushed.git_ref, 1).await.map_or(true, |head| {
2086 head.first().is_none_or(|commit| commit.hash == pushed.after)
2087 }),
2088 };
2089 if moved {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2090 self.publish_git_push(
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2091 &repo,
2092 &pushed.git_ref,
2093 pushed.before.as_deref(),
2094 &pushed.after,
2095 actor.clone(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2096 unscanned,
Merge branch 'worktree-agent-a3abfcce648e87dca'2097 caused_by_job.clone(),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2098 )
2099 .await?;
2100 }
2101 }
2102 Ok(())
2103 }
2104}
2105
2106/// A push the store accepted, to be recorded once git has its answer.
2107struct PushDone {
2108 repo: Repo,
2109 pushed: Vec<git_http::Pushed>,
2110 pack_bytes: u64,
2111 actor: Option<String>,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2112 /// Too large to scan for secrets before it was stored.
2113 unscanned: bool,
Merge branch 'worktree-agent-a3abfcce648e87dca'2114 /// The run whose job's token pushed, if one did: its push starts no
2115 /// workflows.
2116 caused_by_job: Option<String>,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2117}
2118
2119/// What a git request leaves for after its answer: its audit entry, with
2120/// how the request ended, and what a push changed.
2121struct AfterGit {
2122 audit: Option<Box<g1t_contracts::audit::NewAuditEntry>>,
2123 status: u16,
2124 message: Option<String>,
2125 push: Option<PushDone>,
2126}
2127
2128impl AfterGit {
2129 fn ended(&mut self, status: u16, message: Option<String>) {
2130 self.status = status;
2131 self.message = message;
2132 }
2133
2134 /// Does the work once the response is on its way. A failure is logged:
2135 /// git has already been told how its request went.
2136 fn spawn(self, env: &Env, ctx: &Context) {
2137 if self.audit.is_none() && self.push.is_none() {
2138 return;
2139 }
2140 let env = env.clone();
2141 ctx.wait_until(async move {
2142 let repos = match service(&env) {
2143 Ok(repos) => repos,
2144 Err(error) => {
2145 worker::console_error!("git request not recorded: {error}");
2146 return;
Events service in Rust, with RFC 3339 times and accurate push events2147 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2148 };
2149 repos.finish_git(self.audit, self.status, self.message).await;
2150 if let Some(push) = self.push
2151 && let Err(error) = repos.record_push(push).await
2152 {
2153 worker::console_error!("push not recorded: {error}");
Rust repos service with shipping; pull requests kept in the model2154 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2155 });
Rust repos service with shipping; pull requests kept in the model2156 }
2157}
2158
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2159fn service(env: &Env) -> Result<Repos<ArtifactsStore>> {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2160 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 look2161 Ok(Repos {
Rust repos service with shipping; pull requests kept in the model2162 registry: Registry { db: env.d1("DB")? },
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2163 store: ArtifactsStore::new(env, shared.clone())?,
2164 shared,
Merge branch 'worktree-agent-aaf03bdceac799c89'2165 packs: pack_cache::Packs::from_env(env).map(Rc::new),
Events service in Rust, with RFC 3339 times and accurate push events2166 events: env.service("EVENTS")?,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API2167 security: env.service("SECURITY").ok(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2168 billing: env.service("BILLING").ok(),
2169 identity: env.service("IDENTITY").ok(),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2170 work: env.service("WORK").ok(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2171 free_private_bytes: git_ops::free_private_bytes(env),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2172 fork_days: forks::retention_days(env),
2173 repo_limit: env
2174 .var("REPO_STORAGE_LIMIT_BYTES")
2175 .ok()
2176 .and_then(|value| value.to_string().parse().ok())
2177 .unwrap_or(pack_limits::DEFAULT_REPO_LIMIT_BYTES),
2178 large_pushes: git_http::LargePushes::from_var(env.var("LARGE_PUSHES").ok().map(|value| value.to_string()).as_deref()),
2179 placement: shards::Placement::from_vars(
2180 env.var("ARTIFACTS_NEW_REPOS").ok().map(|value| value.to_string()).as_deref(),
2181 env.var("ARTIFACTS_EU_NAMESPACE").ok().map(|value| value.to_string()).as_deref(),
2182 ),
Merge branch 'worktree-agent-a2013627e5ea4ab13'2183 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 look2184 })
2185}
2186
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2187/// Writes what this isolate metered once the answer has gone back, every
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2188/// few seconds at most: now, or once it is due, waiting in this request's
2189/// `wait_until` so nothing counted is left for a request that may never
2190/// come (meters.rs).
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2191fn flush_later(env: &Env, ctx: &Context) {
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2192 let Some(wait) = meters::plan_flush() else {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2193 return;
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2194 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2195 if let Ok(db) = env.d1("DB") {
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2196 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 daily2197 }
2198}
2199
Merge branch 'worktree-agent-ac5b181a013e54348'2200/// `/backups/<job id>/parts/<number>`: the job and the part's number.
2201fn backup_part_path(path: &str) -> Option<(String, u16)> {
2202 let rest = path.strip_prefix("/backups/")?;
2203 let (job, number) = rest.split_once("/parts/")?;
2204 let number = number.parse::<u16>().ok()?;
2205 (!job.is_empty() && !job.contains('/')).then(|| (job.to_owned(), number))
2206}
2207
2208fn backups_off<T>() -> Outcome<T> {
2209 Outcome::fail(FailureCode::Conflict, "Backups are off on this installation: it has no storage for them.")
2210}
2211
2212/// One part of a backup's bundle, with the job's token in its header.
2213async fn backup_part(request: &mut Request, env: &Env, repos: &Repos<ArtifactsStore>, job_id: String, number: u16) -> Result<Response> {
2214 let Some(blobs) = backups::storage(env) else {
2215 return reply(&backups_off::<()>());
2216 };
2217 let token = request.headers().get(g1t_contracts::backups::TOKEN_HEADER)?.unwrap_or_default();
2218 let bytes = request.bytes().await?;
2219 let job = g1t_contracts::backups::BackupJobArgs { job_id, token };
2220 reply(&backups::part(&repos.registry.db, &blobs, &job, number, bytes).await?)
2221}
2222
2223#[cfg(test)]
2224mod backup_path_tests {
2225 use super::backup_part_path;
2226
2227 #[test]
2228 fn a_part_is_named_by_its_job_and_number() {
2229 assert_eq!(backup_part_path("/backups/bkp_1/parts/3"), Some(("bkp_1".to_owned(), 3)));
2230 assert_eq!(backup_part_path("/backups/bkp_1/parts/x"), None);
2231 assert_eq!(backup_part_path("/backups//parts/1"), None);
2232 assert_eq!(backup_part_path("/acme/rocket.git/info/refs"), None);
2233 }
2234}
2235
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2236/// Read methods whose answer is an `Outcome`: when the git store is busy,
2237/// the site is told so in words instead of failing the page.
2238const OUTCOME_READS: [&str; 6] = ["tree", "blob", "log", "branches", "blame", "compare"];
2239
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2240#[event(fetch)]
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2241async fn fetch(mut request: Request, env: Env, ctx: Context) -> Result<Response> {
Fast pages, required checks on the branch, self-hosted runners, honest incidents2242 let mut repos = service(&env)?;
Merge branch 'worktree-agent-ac5b181a013e54348'2243 // A part of a backup's bundle, as the API passes it on from the
2244 // sandbox: bytes, not JSON (backups.rs).
2245 if request.method() == Method::Put
2246 && let Some((job_id, number)) = backup_part_path(&request.path())
2247 {
2248 let answered = backup_part(&mut request, &env, &repos, job_id, number).await;
2249 flush_later(&env, &ctx);
2250 return answered;
2251 }
Rust repos service with shipping; pull requests kept in the model2252 let Some(method) = rpc_method(&request) else {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2253 let answered = repos.git_http(request, &env, &ctx).await;
2254 flush_later(&env, &ctx);
2255 return answered;
Rust repos service with shipping; pull requests kept in the model2256 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents2257 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
2258 // Git over HTTPS above always reads the primary.
2259 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
2260 repos.registry.db = db;
Rust repos service with shipping; pull requests kept in the model2261 let body: serde_json::Value = request.json().await?;
2262
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2263 let answered = async { match method.as_str() {
Rust repos service with shipping; pull requests kept in the model2264 "get" => reply(&repos.get(args(body)?).await?),
2265 "get_by_id" => reply(&repos.get_by_id(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2266 "readable" => {
2267 let a: ReadableArgs = args(body)?;
2268 reply(&repos.registry.readable(&a.ids, &a.viewer).await?)
2269 }
2270 "public_namespaces" => {
2271 let a: PublicNamespacesArgs = args(body)?;
2272 reply(&repos.registry.public_namespaces(&a.owner_id).await?)
2273 }
Automations: rules in .g1t/automations that act when something happens2274 "path_by_id" => {
2275 let a: PathByIdArgs = args(body)?;
2276 reply(
2277 &repos
2278 .registry
2279 .by_id(&a.id)
2280 .await?
2281 .filter(|repo| repo.fork_of.is_none())
2282 .map(|repo| RepoPath {
2283 namespace: repo.namespace,
2284 name: repo.name,
2285 }),
2286 )
2287 }
Rust repos service with shipping; pull requests kept in the model2288 "list" => {
2289 let a: ListArgs = args(body)?;
2290 reply(
2291 &repos
2292 .registry
Workspaces own repositories2293 .list(
2294 &a.viewer,
2295 a.query.as_deref(),
2296 a.namespace.as_deref(),
2297 a.member_only,
2298 )
Rust repos service with shipping; pull requests kept in the model2299 .await?,
2300 )
2301 }
2302 "create" => reply(&repos.create(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2303 // Services only: a GitHub mirror catching up, or pushing out.
2304 "mirror" => reply(&repos.mirror(args(body)?).await?),
2305 "transfer" => reply(&repos.transfer(args(body)?).await?),
2306 // A repository's lifecycle: see lifecycle.rs.
2307 "delete" => reply(&repos.delete(args(body)?).await?),
2308 "deleted" => reply(&repos.deleted(args(body)?).await?),
2309 "restore" => reply(&repos.restore(args(body)?).await?),
2310 "purge" => reply(&repos.purge(args(body)?).await?),
2311 "purge_due" => reply(&repos.purge_due(args(body)?).await?),
2312 "rename" => reply(&repos.rename(args(body)?).await?),
2313 "archive" => reply(&repos.archive(args(body)?).await?),
2314 "set_visibility" => reply(&repos.set_visibility(args(body)?).await?),
2315 "set_default_branch" => reply(&repos.set_default_branch(args(body)?).await?),
2316 "rename_branch" => reply(&repos.rename_branch(args(body)?).await?),
2317 "resolve_branch" => reply(&repos.resolve_branch(args(body)?).await?),
2318 "status_by_id" => reply(&repos.status_by_id(args(body)?).await?),
2319 "resolve_path" => {
2320 let a: ResolvePathArgs = args(body)?;
2321 reply(&repos.registry.resolve_moved(&a.path).await?)
2322 }
2323 "namespace_count" => {
2324 let a: NamespaceCountArgs = args(body)?;
2325 reply(&repos.registry.count_in(&a.namespace).await?)
2326 }
Agents as a team: lifecycle, merge queue, billing and a new shell2327 "update" => reply(&repos.update(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model2328 "tree" => reply(&repos.tree(args(body)?).await?),
2329 "blob" => reply(&repos.blob(args(body)?).await?),
2330 "log" => reply(&repos.log(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell2331 "blame" => reply(&repos.blame(args(body)?).await?),
Issues and pull requests replace intents and attempts2332 "fork_for_pull" => reply(&repos.fork_for_pull(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model2333 "git_access" => reply(&repos.git_access(args(body)?).await?),
Pull requests from branches2334 "branches" => reply(&repos.branches(args(body)?).await?),
Branches and Tags pages, each file's last commit, and the branch menu on files2335 "last_commits" => reply(&repos.last_commits(args(body)?).await?),
2336 "tags" => reply(&repos.tags(args(body)?).await?),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb972337 // The About: what the Files page shows beside the files (about.rs).
2338 // What is kept behind the head is worked out again after the answer.
2339 "about" => {
2340 let (answer, refresh) = repos.about(args(body)?).await?;
2341 about::refresh_later(&env, &ctx, refresh);
2342 reply(&answer)
2343 }
2344 "languages" => {
2345 let (answer, refresh) = repos.languages(args(body)?).await?;
2346 about::refresh_later(&env, &ctx, refresh);
2347 reply(&answer)
2348 }
2349 "contributors" => {
2350 let (answer, refresh) = repos.contributors(args(body)?).await?;
2351 about::refresh_later(&env, &ctx, refresh);
2352 reply(&answer)
2353 }
2354 "license" => {
2355 let (answer, refresh) = repos.license(args(body)?).await?;
2356 about::refresh_later(&env, &ctx, refresh);
2357 reply(&answer)
2358 }
2359 "stars" => reply(&repos.stars(args(body)?).await?),
2360 "star" => reply(&repos.star(args(body)?).await?),
2361 "stargazers" => reply(&repos.stargazers(args(body)?).await?),
2362 "starred" => reply(&repos.starred(args(body)?).await?),
2363 "releases" => reply(&repos.releases(args(body)?).await?),
2364 "release" => reply(&repos.release(args(body)?).await?),
2365 "create_release" => reply(&repos.create_release(args(body)?).await?),
2366 "update_release" => reply(&repos.update_release(args(body)?).await?),
2367 "delete_release" => reply(&repos.delete_release(args(body)?).await?),
Pull requests from branches2368 "head" => reply(&repos.head(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell2369 "behind" => reply(&repos.behind(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2370 "divergence" => reply(&repos.divergence(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model2371 "land" => reply(&repos.land(args(body)?).await?),
Catching up with main takes seconds when the two sides touched different files2372 "update_pull_branch" => reply(&repos.update_pull_branch(args(body)?).await?),
Merge queue: tested states are deleted once their entry leaves2373 "delete_branch" => reply(&repos.delete_branch(args(body)?).await?),
Fast pages, required checks on the branch, self-hosted runners, honest incidents2374 "commit_file" => reply(&repos.commit_file(args(body)?).await?),
Diffs on attempts; hosted agent presented as the g1t agent2375 "compare" => reply(&repos.compare(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2376 // Services only: a pull request's commits, as rules look at them (rules.rs).
2377 "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 API2378 "scan_history" => reply(&repos.scan_history(args(body)?).await?),
2379 "find_lockfiles" => reply(&repos.find_lockfiles(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2380 "match_pattern" => reply(&repos.match_pattern(args(body)?).await?),
2381 "check_secret" => reply(&repos.check_secret(args(body)?).await?),
Search across all of g1t, Explore, and a command palette2382 "list_files" => reply(&repos.list_files(args(body)?).await?),
2383 "changed_files" => reply(&repos.changed_files(args(body)?).await?),
2384 "read_blobs" => reply(&repos.read_blobs(args(body)?).await?),
Composer from the workspace's own repositories, and go get from g1t.sh2385 // Services only: what the Composer registry builds packages from.
2386 "refs" => reply(&repos.refs_of(args(body)?).await?),
2387 "raw_file" => reply(&repos.raw_file(args(body)?).await?),
2388 "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 put2389 "visibility" => {
2390 let a: g1t_contracts::repos::VisibilityArgs = args(body)?;
2391 reply(&repos.registry.visibility(&a.paths).await?)
2392 }
2393 "storage" => reply(&repos.registry.storage().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2394 "git_operations" => {
2395 let a: GitOperationsArgs = args(body)?;
2396 reply(&git_ops::totals(&repos.registry.db, &a.month, a.since.as_deref(), a.namespace.as_deref().map(str::to_lowercase).as_deref()).await?)
2397 }
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA2398 // Identity, once: who created each repository (members.rs there).
2399 "repo_creators" => {
2400 let a: AllIdsArgs = args(body)?;
2401 let limit = a.limit.clamp(1, 500);
2402 let repos = repos.registry.creators_after(a.after.as_deref(), limit).await?;
2403 let next = (repos.len() == limit as usize).then(|| repos.last().map(|repo| repo.id.clone())).flatten();
2404 reply(&CreatorPage { repos, next })
2405 }
Search across all of g1t, Explore, and a command palette2406 "all_ids" => {
2407 let a: AllIdsArgs = args(body)?;
2408 let limit = a.limit.clamp(1, 500);
2409 let ids = repos.registry.ids_after(a.after.as_deref(), limit).await?;
2410 let next = (ids.len() == limit as usize).then(|| ids.last().cloned()).flatten();
2411 reply(&IdPage { ids, next })
2412 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2413 // The raw meters of the git store, for reconciling with Cloudflare
2414 // (meters.rs, scripts/ops/artifacts-usage.mjs).
2415 "artifacts_usage" => {
2416 let a: meters::UsageArgs = args(body)?;
2417 reply(&meters::usage(&repos.registry.db, &a).await?)
2418 }
2419 "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)2420 // Billing: the workspace each pull request's working copy is counted
2421 // for, so Cloudflare's own count of `pulls--<id>` shares out too.
2422 "pull_owners" => {
2423 #[derive(serde::Deserialize)]
2424 struct PullOwnersArgs {
2425 pulls: Vec<String>,
2426 }
2427 let a: PullOwnersArgs = args(body)?;
2428 let pulls: Vec<String> = a.pulls.into_iter().take(500).collect();
2429 reply(&serde_json::json!({ "owners": meters::pull_owners(&repos.registry.db, &pulls).await? }))
2430 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2431 // Services only: which meters are operations, changed without a deploy.
2432 "set_operation_mapping" => {
2433 let row: meters::MappingRow = args(body)?;
2434 meters::set_mapping(&repos.registry.db, &row, &rfc3339(now_ms())).await?;
2435 reply(&meters::read_mapping(&repos.registry.db).await?)
2436 }
Merge branch 'worktree-agent-ac5b181a013e54348'2437 // Backups (backups.rs): the runner's sweep claims queued ones, and
2438 // each sandbox, through the API, asks for its job and says how it went.
2439 "claim_backups" => {
2440 let a: g1t_contracts::backups::ClaimBackupsArgs = args(body)?;
2441 let blobs = backups::storage(&env);
2442 reply(&backups::claim(&repos.registry.db, blobs.as_ref(), &a, now_ms()).await?)
2443 }
2444 "backup_spec" => match backups::storage(&env) {
2445 Some(blobs) => {
2446 let a: g1t_contracts::backups::BackupJobArgs = args(body)?;
2447 let every = backups::Settings::from_env(&env).full_every;
2448 reply(&backups::spec(&repos.registry, &blobs, &repos.store, &a, every, now_ms()).await?)
2449 }
2450 None => reply(&backups_off::<bool>()),
2451 },
2452 "backup_complete" => match backups::storage(&env) {
2453 Some(blobs) => reply(&backups::complete(&repos.registry, &blobs, &args(body)?, now_ms()).await?),
2454 None => reply(&backups_off::<bool>()),
2455 },
2456 "backup_fail" => match backups::storage(&env) {
2457 Some(blobs) => reply(&backups::fail(&repos.registry.db, &blobs, &args(body)?).await?),
2458 None => reply(&backups_off::<bool>()),
2459 },
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2460 // How the git store has been answering, for the status page.
2461 "store_health" => {
2462 let a: meters::HealthArgs = args(body)?;
2463 reply(&meters::health(&repos.registry.db, &a).await?)
2464 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'2465 // Where repositories may be kept, for a workspace's settings.
2466 "storage_options" => reply(&repos.storage_options()),
2467 // Services and operators only: how each namespace stands, and
2468 // moving a repository between them (namespaces.rs, moves.rs).
2469 "namespaces" => reply(&repos.standings().await?),
2470 "move_repository" => reply(&repos.move_repository(args(body)?).await?),
2471 "repository_moves" => {
2472 let a: moves::ListMovesArgs = args(body)?;
2473 reply(&repos.registry.moves(a.limit.unwrap_or(50)).await?)
2474 }
Rust repos service with shipping; pull requests kept in the model2475 _ => Response::error("Unknown method", 404),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2476 } }
2477 .await;
2478 // The git store is busy: said in words, with when to try again.
2479 let answered = match answered {
2480 Err(error) => match resilience::busy(&error.to_string()) {
2481 Some(busy) if OUTCOME_READS.contains(&method.as_str()) => {
2482 reply(&Outcome::<()>::fail(FailureCode::Conflict, busy.message().trim()))
2483 }
2484 Some(busy) => {
2485 let response = Response::error(busy.message(), 503)?;
2486 response.headers().set("retry-after", &busy.retry_after.to_string())?;
2487 Ok(response)
2488 }
2489 None => Err(error),
2490 },
2491 answered => answered,
Fast pages, required checks on the branch, self-hosted runners, honest incidents2492 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2493 flush_later(&env, &ctx);
Fast pages, required checks on the branch, self-hosted runners, honest incidents2494 served.finish(answered)
Rust repos service with shipping; pull requests kept in the model2495}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2496
Merge branch 'worktree-agent-ac5b181a013e54348'2497/// The nightly cron in wrangler.jsonc: tonight's backups are queued.
2498const BACKUP_CRON: &str = "53 2 * * *";
2499
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2500/// The hourly sweep: deleted repositories whose time to be restored has
Merge branch 'worktree-agent-ac5b181a013e54348'2501/// passed are purged. See lifecycle.rs. And, at [`BACKUP_CRON`], the
2502/// 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 look2503#[event(scheduled)]
Merge branch 'worktree-agent-ac5b181a013e54348'2504async fn scheduled(event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2505 let repos = match service(&env) {
2506 Ok(repos) => repos,
2507 Err(error) => {
2508 worker::console_error!("repos: the sweep could not start: {error}");
2509 return;
2510 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2511 };
Merge branch 'worktree-agent-ac5b181a013e54348'2512 if event.cron() == BACKUP_CRON {
2513 let Some(blobs) = backups::storage(&env) else { return };
2514 match backups::nightly(&repos.registry.db, &blobs, backups::Settings::from_env(&env), now_ms()).await {
2515 Ok(night) => worker::console_log!("repos: queued {} backups, removed {} of purged repositories", night.queued, night.pruned),
2516 Err(error) => worker::console_error!("repos: backups could not be queued: {error}"),
2517 }
2518 return;
2519 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2520 match repos.purge_due(PurgeDueArgs::default()).await {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2521 Ok(0) => {}
2522 Ok(count) => worker::console_log!("repos: purged {count} deleted repositories"),
2523 Err(error) => worker::console_error!("repos: the purge sweep failed: {error}"),
2524 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2525 // Pull requests' working copies whose time has come (forks.rs).
2526 match repos.retire_due().await {
2527 Ok(0) => {}
2528 Ok(count) => worker::console_log!("repos: removed {count} pull request working copies"),
2529 Err(error) => worker::console_error!("repos: the working copy sweep failed: {error}"),
2530 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'2531 // Repositories moving between namespaces, and old copies (moves.rs).
2532 match repos.run_moves().await {
2533 Ok(0) => {}
2534 Ok(count) => worker::console_log!("repos: moved {count} repositories between namespaces"),
2535 Err(error) => worker::console_error!("repos: the move sweep failed: {error}"),
2536 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2537 meters::flush(&repos.registry.db).await;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2538}
2539
2540/// Events from the bus. A workspace's rename: its repositories move to the
2541/// workspace's current slug, asked of identity by id, so a repeated or late
2542/// 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 member2543/// were. A workspace's deletion: its repositories are deleted with it,
2544/// restored with it, or purged with it.
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2545#[event(queue)]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2546async fn queue(batch: MessageBatch<Event>, env: Env, ctx: Context) -> Result<()> {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2547 let registry = Registry { db: env.d1("DB")? };
2548 let identity = env.service("IDENTITY")?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2549 let handled = handle_events(&batch, &env, &registry, &identity).await;
2550 flush_later(&env, &ctx);
2551 handled
2552}
2553
2554async fn handle_events(batch: &MessageBatch<Event>, env: &Env, registry: &Registry, identity: &Fetcher) -> Result<()> {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2555 for message in batch.messages()? {
2556 let event = message.body();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2557 // A pull request merged, closed or reopened: its working copy is
2558 // kept or let go (forks.rs).
2559 if let Some(change) = forks::pull_change(&event.kind) {
2560 let Some(pull_id) = forks::pull_id_of(&event.data) else {
2561 worker::console_error!("{} {} names no pull request", event.kind, event.id);
2562 continue;
2563 };
2564 let repos = service(env)?;
2565 match change {
2566 forks::PullChange::Settled => repos.pull_settled(&pull_id).await?,
2567 forks::PullChange::Reopened => repos.pull_reopened(&pull_id).await?,
2568 }
2569 continue;
2570 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member2571 // A workspace deleted, restored or purged: its repositories go with
2572 // it, come back with it, or are purged with it (lifecycle.rs).
2573 if event.kind == "workspace.deleting" {
2574 match serde_json::from_value::<WorkspaceDeleting>(event.data.clone()) {
2575 Ok(deleting) => service(env)?.delete_with_workspace(&deleting, &protected_workspaces(env)).await?,
2576 Err(_) => worker::console_error!("workspace.deleting {} could not be read", event.id),
2577 }
2578 continue;
2579 }
2580 if event.kind == "workspace.restored" {
2581 match serde_json::from_value::<WorkspaceRestored>(event.data.clone()) {
2582 Ok(restored) => service(env)?.restore_with_workspace(&restored).await?,
2583 Err(_) => worker::console_error!("workspace.restored {} could not be read", event.id),
2584 }
2585 continue;
2586 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2587 if event.kind == "workspace.deleted" {
2588 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 member2589 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 look2590 Err(_) => worker::console_error!("workspace.deleted {} could not be read", event.id),
2591 }
2592 continue;
2593 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2594 if event.kind != "workspace.renamed" {
2595 continue;
2596 }
2597 let Ok(renamed) = serde_json::from_value::<WorkspaceRenamed>(event.data.clone()) else {
2598 worker::console_error!("workspace.renamed {} could not be read", event.id);
2599 continue;
2600 };
2601 let names: HashMap<String, String> = g1t_kit::call(
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2602 identity,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2603 "usernames",
2604 &g1t_contracts::identity::UsernamesArgs {
2605 ids: vec![renamed.workspace_id.clone()],
2606 },
2607 )
2608 .await?;
2609 let current = names
2610 .get(&renamed.workspace_id)
2611 .cloned()
2612 .unwrap_or_else(|| renamed.to.clone());
2613 let left = registry
2614 .rename_namespace(&renamed.stale_slugs(&current), &current)
2615 .await?;
2616 if left > 0 {
2617 worker::console_error!(
2618 "{left} repositories stayed under {} or {}: {current} already has repositories of the same names",
2619 renamed.from,
2620 renamed.to
2621 );
2622 }
2623 }
2624 Ok(())
2625}
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2626
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member2627/// The workspaces whose repositories never go with a deletion, whatever is
2628/// published: `PROTECTED_WORKSPACES` if set here, and Flagon's always.
2629fn protected_workspaces(env: &Env) -> Vec<String> {
2630 let configured = env.var("PROTECTED_WORKSPACES").ok().map(|v| v.to_string());
2631 g1t_contracts::identity::protected_names(configured.as_deref())
2632}
2633
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2634/// The repository a push to a path that does not exist yet creates: private,
2635/// so nothing pushed by mistake is published. An owner makes it public on
2636/// purpose (`POST /repos/{owner}/{repo}/visibility`).
2637fn push_to_create(owner: &User, path: &RepoPath) -> CreateArgs {
2638 CreateArgs {
2639 owner: owner.clone(),
2640 namespace: path.namespace.clone(),
2641 name: path.name.clone(),
2642 description: None,
2643 is_private: true,
2644 import_url: None,
2645 import_token: None,
2646 }
2647}
2648
2649#[cfg(test)]
2650mod push_to_create_tests {
2651 use super::*;
2652
2653 #[test]
2654 fn a_pushed_repository_starts_private() {
2655 let owner: User = serde_json::from_value(serde_json::json!({ "id": "usr_1", "username": "ada" })).unwrap();
2656 let args = push_to_create(&owner, &RepoPath { namespace: "acme".into(), name: "site".into() });
2657 assert!(args.is_private);
2658 assert_eq!((args.namespace.as_str(), args.name.as_str()), ("acme", "site"));
2659 }
2660}

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