Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
3,136 linesCodeBlameRaw

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

Prices are what g1t pays plus 20%, from the first second1//! 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;
Prices are what g1t pays plus 20%, from the first second10mod blame;
11mod 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;
Prices are what g1t pays plus 20%, from the first second15mod diff;
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 2516mod drift;
Merge branch 'worktree-agent-a2013627e5ea4ab13'17mod fallback;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily18mod forks;
Prices are what g1t pays plus 20%, from the first second19mod git_http;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look20mod git_ops;
Prices are what g1t pays plus 20%, from the first second21mod import;
22mod land;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9723mod languages;
Branches and Tags pages, each file's last commit, and the branch menu on files24mod last_commits;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9725mod license;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look26mod lifecycle;
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails27mod limits;
Prices are what g1t pays plus 20%, from the first second28mod listing;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily29mod meters;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look30mod mirror;
Merge branch 'worktree-agent-a2013627e5ea4ab13'31mod moves;
32mod namespaces;
Merge branch 'worktree-agent-a1b995daa94e4e1b7'33mod pack_cache;
Fast pages, required checks on the branch, self-hosted runners, honest incidents34mod pack_limits;
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer35mod push_checks;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar36mod push_commits;
Prices are what g1t pays plus 20%, from the first second37mod refs;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms38mod refs_cache;
Prices are what g1t pays plus 20%, from the first second39mod registry;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily40mod resilience;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge41mod rule_facts;
42mod rules;
Prices are what g1t pays plus 20%, from the first second43mod run_access;
44mod secret_scan;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily45mod shards;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms46mod shared;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge47mod signatures;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9748mod stats;
Prices are what g1t pays plus 20%, from the first second49mod store;
A pull request's `pull_request` runs build the pull request merged into its base branch, as on GitHub. g1t keeps `refs/pull/<number>/head` at every open pull request's head (a fork's or working copy's head commit is copied into the repository) and `refs/pull/<number>/merge` at a test merge commit of that head into the base branch's latest commit, moved whenever the head is pushed to, the pull request is opened or reopened, or its base changes; both can be fetched from the repository. A run's `github.sha` and `GITHUB_SHA` are that merge commit, `github.event.pull_request.merge_commit_sha` names it, `actions/checkout` fetches it as `+<sha>:refs/remotes/pull/<number>/merge` from the repository itself, and the workflow files and local reusable workflows are read from it; the run, its statuses and its check runs stay on the head, so required checks hold merges as before, and `pull_request_target` is unchanged. A pull request with conflicts starts no `pull_request` workflows, as on GitHub, and its checks box says why. g1t makes the merge without a sandbox, merging a file both sides changed line by line as git does; what it cannot merge itself (a rename on one side against a change on the other, more than 300 changed files) builds the head as before. The Actions and Checks guides say so.50mod test_merge;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look51mod transfer;
Workflow files need workflow_files:write from a token; fine-grained permission table52mod workflow_gate;
Prices are what g1t pays plus 20%, from the first second53
54use g1t_contracts::events::{
Merge branch 'worktree-agent-a8b78c62188153aa3' into claude/cool-newton-7nm8r955 Event, GitDeleted, 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 member56 WorkspaceDeleting, WorkspaceRenamed, WorkspaceRestored,
Prices are what g1t pays plus 20%, from the first second57};
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look58use g1t_contracts::access::{self, Capability};
Prices are what g1t pays plus 20%, from the first second59use g1t_contracts::repos::*;
60use g1t_contracts::time::rfc3339;
Merge main (membership, two-factor, GitHub repo roles) into tokens61use g1t_contracts::{FailureCode, Outcome, PrincipalKind, Role, User, Viewer, is_valid_repo_name, new_id};
Prices are what g1t pays plus 20%, from the first second62use g1t_kit::{args, now_ms, reply, rpc_method};
63use std::collections::{HashMap, HashSet, VecDeque};
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms64use std::rc::Rc;
Prices are what g1t pays plus 20%, from the first second65
66use serde::Serialize;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look67use worker::{
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails68 Context, Env, Fetcher, MessageBatch, MessageExt, Method, Request, Response, Result, ScheduleContext, ScheduledEvent,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look69 event,
70};
Prices are what g1t pays plus 20%, from the first second71
72use registry::{Registry, can_read, can_write, store_key};
73use store::{ArtifactsStore, GitRepo, GitStore, Scope};
74
75/// 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 daily76pub(crate) const PULLS_NAMESPACE: &str = "pulls";
Prices are what g1t pays plus 20%, from the first second77const MAX_TEXT_BYTES: usize = 512 * 1024;
78/// How far back a pull request may have forked and still be landed.
79const MAX_ANCESTRY: u32 = 1000;
Branches and Tags pages, each file's last commit, and the branch menu on files80/// The most tags a repository's Tags page reads and lists.
81const MAX_TAGS_READ: usize = 100;
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 2582/// Branch heads measured in one `branch_drift` call.
83const MAX_DRIFT_HEADS: usize = 100;
Merge last commits over the whole history: progress kept per ref and path, a push reads only its own commits84/// How long a `last_commits` walk asked without a budget runs before it
85/// stops and keeps its progress for the next call. The site asks from a
86/// waitUntil, which may run 30 s past its response.
87const LAST_COMMITS_WALK_MS: u64 = 20_000;
Branches and Tags pages, each file's last commit, and the branch menu on files88
89/// One path segment, percent-encoded for a cache key.
90fn urlencoding_segment(segment: &str) -> String {
91 segment
92 .bytes()
93 .map(|b| if b.is_ascii_alphanumeric() || b"-._~".contains(&b) { (b as char).to_string() } else { format!("%{b:02X}") })
94 .collect()
95}
Prices are what g1t pays plus 20%, from the first second96const MAX_DESCRIPTION_CHARS: usize = 200;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look97pub(crate) const SOURCE: &str = "repos";
98pub(crate) const UNVERIFIED: &str = "Confirm your email address first. Check your inbox, or resend the link from the banner on g1t.sh.";
Prices are what g1t pays plus 20%, from the first second99
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look100pub(crate) fn not_found<T>() -> Outcome<T> {
Prices are what g1t pays plus 20%, from the first second101 Outcome::fail(FailureCode::NotFound, "Repository not found.")
102}
103
104/// Decoded text, or `None` when the file is too large or looks binary.
105/// Whether a ref is a full commit hash rather than a branch name.
106fn is_commit_hash(git_ref: &str) -> bool {
107 git_ref.len() == 40 && git_ref.bytes().all(|b| b.is_ascii_hexdigit())
108}
109
110fn text_of(bytes: Vec<u8>) -> Option<String> {
111 if bytes.len() > MAX_TEXT_BYTES || bytes.contains(&0) {
112 return None;
113 }
114 Some(String::from_utf8_lossy(&bytes).into_owned())
115}
116
117fn is_readme(name: &str) -> bool {
118 matches!(
119 name.to_lowercase().as_str(),
120 "readme" | "readme.md" | "readme.markdown" | "readme.txt"
121 )
122}
123
124/// Whether `ancestor` is reachable from the newest commit in `history`.
125///
126/// `history` is the first-parent chain, which is all the store lists; a fork
127/// that merged the target branch in has the target's head on a second
128/// parent, so the walk follows every parent.
129async fn descends_from<R: GitRepo>(repo: &R, history: &[Commit], ancestor: &str) -> Result<bool> {
130 let known: HashMap<&str, &[String]> = history
131 .iter()
132 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
133 .collect();
134 let mut seen = HashSet::new();
135 let mut queue: Vec<String> = history
136 .first()
137 .map(|c| c.hash.clone())
138 .into_iter()
139 .collect();
140 while let Some(hash) = queue.pop() {
141 if hash == ancestor {
142 return Ok(true);
143 }
144 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
145 continue;
146 }
147 match known.get(hash.as_str()) {
148 Some(parents) => queue.extend(parents.iter().cloned()),
149 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
150 }
151 }
152 Ok(false)
153}
154
155/// The commit closest to the newest in `history` that is also in `shared`:
156/// where a fork and the repository it came from last agreed.
157async fn nearest_ancestor_in<R: GitRepo>(
158 repo: &R,
159 history: &[Commit],
160 shared: &HashSet<String>,
161) -> Result<Option<String>> {
162 let known: HashMap<&str, &[String]> = history
163 .iter()
164 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
165 .collect();
166 let mut seen = HashSet::new();
167 let mut queue: VecDeque<String> = history
168 .first()
169 .map(|c| c.hash.clone())
170 .into_iter()
171 .collect();
172 while let Some(hash) = queue.pop_front() {
173 if shared.contains(&hash) {
174 return Ok(Some(hash));
175 }
176 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
177 continue;
178 }
179 match known.get(hash.as_str()) {
180 Some(parents) => queue.extend(parents.iter().cloned()),
181 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
182 }
183 }
184 Ok(None)
185}
186
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily187thread_local! {
188 /// Targets' sides of mergeability, by head (coalesce.rs).
189 static TARGETS: std::cell::RefCell<coalesce::Memo<coalesce::TargetKey, Rc<coalesce::TargetSide>>> =
190 std::cell::RefCell::new(coalesce::Memo::new(coalesce::TARGET_TTL_MS, 32));
191 /// What targets changed between two trees.
192 static THEIRS: std::cell::RefCell<coalesce::Memo<coalesce::TheirsKey, (Vec<String>, bool)>> =
193 std::cell::RefCell::new(coalesce::Memo::new(coalesce::THEIRS_TTL_MS, 256));
194 /// What repositories hold, as read for a push's first request, for the
195 /// same push's second: a push's POST does not wait on the database.
196 static HELD: std::cell::RefCell<coalesce::Memo<String, u64>> =
197 std::cell::RefCell::new(coalesce::Memo::new(60_000, 512));
198}
199
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look200pub(crate) struct Repos<S: GitStore> {
Prices are what g1t pays plus 20%, from the first second201 registry: Registry,
202 store: S,
203 events: Fetcher,
204 /// Asked during a push which secrets have been allowed.
205 security: Option<Fetcher>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look206 /// Asked whether a workspace is on a plan, for its private storage.
207 billing: Option<Fetcher>,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge208 /// Says which rulesets hold for a change to a branch or tag, and keeps
209 /// how they judged it (rules.rs). `None` where it is not deployed: the
210 /// old protection flag then holds on push.
211 work: Option<Fetcher>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look212 /// Told when a repository moves, for the tokens of agents at work on it.
213 identity: Option<Fetcher>,
214 /// What a free workspace's private repositories may hold.
215 free_private_bytes: i64,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily216 /// Days a pull request's working copy is kept after it settles (forks.rs).
217 pub(crate) fork_days: u64,
218 /// The most a repository may hold (pack_limits.rs), and what happens
219 /// to a push too large to scan.
220 repo_limit: u64,
221 large_pushes: git_http::LargePushes,
Merge branch 'worktree-agent-a2013627e5ea4ab13'222 /// Which git store namespace new repositories go in (shards.rs), and
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.223 /// the most each should hold (`GITSTORE_NAMESPACE_LIMITS`).
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily224 placement: shards::Placement,
Merge branch 'worktree-agent-a2013627e5ea4ab13'225 limits: HashMap<String, u64>,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms226 /// What isolates share: answers that list refs (refs_cache.rs).
227 shared: Option<Rc<shared::Shared>>,
Merge branch 'worktree-agent-a1b995daa94e4e1b7'228 /// Packs for fresh clones (pack_cache.rs); `None` without the bucket.
Merge branch 'worktree-agent-aaf03bdceac799c89'229 packs: Option<Rc<pack_cache::Packs>>,
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer230 /// What this request started that need not hold up its answer
231 /// (store.rs), handed to its `waitUntil` once it has answered.
232 deferred: Rc<store::Deferred>,
Prices are what g1t pays plus 20%, from the first second233}
234
235impl<S: GitStore> Repos<S> {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms236 /// Records that the refs of the repository with this id changed, once
237 /// they have, so that the answers kept that list them go stale (see
238 /// refs_cache.rs). Everything that changes a repository's refs calls
239 /// this after it (`every_ref_writer_records_the_change` checks). A
240 /// failure is logged: the change itself happened, and what was kept
241 /// expires within `refs_cache::TTL_SECONDS` regardless.
242 pub(crate) async fn refs_moved(&self, repo_id: &str) {
243 if let Err(error) = self.registry.refs_moved(repo_id).await {
244 worker::console_error!("refs of {repo_id} changed but not recorded: {error}");
245 }
246 }
247
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look248 pub(crate) async fn publish<T: Serialize>(&self, event: NewEvent<T>) -> Result<()> {
Prices are what g1t pays plus 20%, from the first second249 g1t_kit::call(
250 &self.events,
251 "publish",
252 &Publish {
253 events: vec![event],
254 },
255 )
256 .await
257 }
258
259 /// Whether the viewer may read `repo`. A pull request's fork of a
260 /// private repository can be read by everyone who can read that
261 /// repository, so its members can review and check out the change, as
262 /// well as by whoever opened the pull request.
263 async fn may_read(&self, repo: &Repo, viewer: &Viewer) -> Result<bool> {
264 if can_read(repo, viewer) {
265 return Ok(true);
266 }
267 let Some(source_id) = &repo.fork_of else {
268 return Ok(false);
269 };
270 Ok(self
271 .registry
272 .by_id(source_id)
273 .await?
274 .is_some_and(|source| can_read(&source, viewer)))
275 }
276
277 /// `repo`, if there is one and the viewer may read it.
278 async fn visible(&self, repo: Option<Repo>, viewer: &Viewer) -> Result<Option<Repo>> {
279 Ok(match repo {
280 Some(repo) if self.may_read(&repo, viewer).await? => Some(repo),
281 _ => None,
282 })
283 }
284
285 /// 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 look286 pub(crate) async fn readable(&self, path: &RepoPath, viewer: &Viewer) -> Result<Option<Repo>> {
Prices are what g1t pays plus 20%, from the first second287 self.visible(self.registry.by_path(path).await?, viewer)
288 .await
289 }
290
291 async fn get(&self, a: GetArgs) -> Result<Outcome<Repo>> {
292 Ok(self
293 .readable(&a.path, &a.viewer)
294 .await?
295 .map_or_else(not_found, Outcome::Ok))
296 }
297
298 async fn get_by_id(&self, a: GetByIdArgs) -> Result<Outcome<Repo>> {
299 Ok(self
300 .visible(self.registry.by_id(&a.id).await?, &a.viewer)
301 .await?
302 .map_or_else(not_found, Outcome::Ok))
303 }
304
305 async fn update(&self, a: UpdateArgs) -> Result<Outcome<Repo>> {
306 let viewer = Some(a.actor.clone());
307 let Some(repo) = self.readable(&a.path, &viewer).await? else {
308 return Ok(not_found());
309 };
Merge main (membership, two-factor, GitHub repo roles) into tokens310 // Its details take Maintain; its protection, Admin; who can see it,
311 // 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 look312 let protection_changes = a.protected.is_some_and(|protected| protected != repo.protected);
313 let details_change = a.description.is_some() || a.website.is_some() || a.topics.is_some();
314 let mut needed = Vec::new();
315 if details_change || !protection_changes {
316 needed.push(Capability::ManageSettings);
317 }
318 if protection_changes {
319 needed.push(Capability::ManageProtection);
320 }
321 let full_name = format!("{}/{}", repo.namespace, repo.name);
322 if repo.fork_of.is_some() {
323 return Ok(Outcome::fail(FailureCode::Forbidden, access::needs(Capability::ManageSettings, &full_name)));
324 }
325 if let Some(missing) = needed.into_iter().find(|capability| !registry::can(&repo, &viewer, *capability)) {
326 return Ok(Outcome::fail(FailureCode::Forbidden, access::needs(missing, &full_name)));
Prices are what g1t pays plus 20%, from the first second327 }
328 if !a.actor.verified {
329 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
330 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look331 if let Some((code, message)) = lifecycle::archived_refusal(&repo) {
332 return Ok(Outcome::fail(code, message));
333 }
Prices are what g1t pays plus 20%, from the first second334 let description = match a.description {
335 Some(text) => Some(
336 text.trim()
337 .chars()
338 .take(MAX_DESCRIPTION_CHARS)
339 .collect::<String>(),
340 )
341 .filter(|text| !text.is_empty()),
342 None => repo.description.clone(),
343 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look344 let website = match a.website.as_deref() {
345 Some(text) => match clean_website(text) {
346 Ok(website) => website,
347 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
348 },
349 None => repo.website.clone(),
350 };
351 // Who can see it is an owner's to change, and a free workspace's
352 // storage may not take it private: see lifecycle.rs.
353 let wants_private = a.is_private.filter(|private| *private != repo.is_private);
354 if wants_private.is_some()
355 && let Err((code, message)) = lifecycle::admin_only(
356 lifecycle::Asker::on(&a.actor, &repo),
357 &repo.namespace,
358 "change the visibility of",
Merge main (membership, two-factor, GitHub repo roles) into tokens359 Capability::ChangeVisibility,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look360 )
361 {
362 return Ok(Outcome::fail(code, message));
363 }
Merge main (membership, two-factor, GitHub repo roles) into tokens364 if let Some(private) = wants_private
365 && let Some(why) = lifecycle::visibility_refusal(&a.actor, &repo, private)
366 {
367 return Ok(Outcome::fail(FailureCode::Forbidden, why));
368 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look369 let is_private = repo.is_private;
Prices are what g1t pays plus 20%, from the first second370 let protected = a.protected.unwrap_or(repo.protected);
371 let topics = match &a.topics {
372 Some(topics) => match clean_topics(topics) {
373 Ok(topics) => topics,
374 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
375 },
376 None => repo.topics.clone(),
377 };
378 self.registry
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look379 .update(&repo.id, description.as_deref(), protected, &topics, website.as_deref())
Prices are what g1t pays plus 20%, from the first second380 .await?;
381 let updated = Repo {
382 description,
383 is_private,
384 protected,
385 topics,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look386 website,
Prices are what g1t pays plus 20%, from the first second387 ..repo
388 };
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge389 // Whether the default branch takes only pull requests is now its
390 // branch protection ruleset's to say (work's rulesets.rs).
391 if let (Some(protected), Some(work)) = (a.protected, &self.work) {
392 #[derive(Serialize)]
393 struct RequirePullRequest<'a> {
394 repo: &'a Repo,
395 protected: bool,
396 actor: &'a User,
397 }
398 let set: Result<Outcome<bool>> =
399 g1t_kit::call(work, "set_requires_pull_request", &RequirePullRequest { repo: &updated, protected, actor: &a.actor }).await;
400 match set {
401 Ok(Outcome::Ok(_)) => {}
402 Ok(Outcome::Fail(failure)) => return Ok(Outcome::Fail(failure)),
403 Err(error) => return Err(error),
404 }
405 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look406 if let Some(private) = wants_private {
407 return self.change_visibility(updated, private, &a.actor, a.surface).await;
408 }
409 let visibility_changed = false;
Prices are what g1t pays plus 20%, from the first second410 // Search and anything else that shows the repository hears of it;
411 // a change of visibility is announced on its own as well, so that
412 // what was public stops being shown at once.
413 self.publish(NewEvent {
414 kind: "repo.updated",
415 source: SOURCE,
416 repo_id: Some(updated.id.clone()),
417 actor: Some(a.actor.id.clone()),
418 data: RepoUpdated {
419 repo_id: updated.id.clone(),
420 namespace: updated.namespace.clone(),
421 name: updated.name.clone(),
422 is_private,
423 visibility_changed,
424 },
425 })
426 .await?;
427 Ok(Outcome::Ok(updated))
428 }
429
430 /// The repository with this id, if it is not a fork, and its store.
431 async fn stored(&self, repo_id: &str) -> Result<Option<S::Repo>> {
432 match self.registry.by_id(repo_id).await? {
433 Some(repo) if repo.fork_of.is_none() => Ok(Some(self.store.open(&store_key(&repo)).await?)),
434 _ => Ok(None),
435 }
436 }
437
438 async fn list_files(&self, a: ListFilesArgs) -> Result<FileList> {
439 let Some(repo) = self.registry.by_id(&a.repo_id).await?.filter(|repo| repo.fork_of.is_none()) else {
440 return Ok(FileList::default());
441 };
442 let git = self.store.open(&store_key(&repo)).await?;
443 let head = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
444 listing::list(&git, None, &head, &a.skip_dirs, a.limit).await
445 }
446
447 async fn changed_files(&self, a: ChangedFilesArgs) -> Result<FileList> {
448 let Some(git) = self.stored(&a.repo_id).await? else {
449 return Ok(FileList::default());
450 };
451 listing::list(&git, a.base.as_deref(), &a.head, &a.skip_dirs, a.limit).await
452 }
453
Composer from the workspace's own repositories, and go get from g1t.sh454 /// Branches and tags with their commits, for g1t's own services.
455 async fn refs_of(&self, a: RefsArgs) -> Result<Option<RepoRefs>> {
456 let Some(repo) = self.registry.by_id(&a.repo_id).await?.filter(|repo| repo.fork_of.is_none()) else {
457 return Ok(None);
458 };
459 let git = self.store.open(&store_key(&repo)).await?;
460 let access = git.access(Scope::Read).await?;
461 let refs = refs::heads_and_tags(refs::all(&access).await?)
462 .into_iter()
463 .map(|(name, commit)| GitRefEntry { name, commit })
464 .collect();
465 Ok(Some(RepoRefs { repo, refs }))
466 }
467
468 async fn raw_file(&self, a: RawFileArgs) -> Result<Option<RawFile>> {
469 use base64::Engine;
470 let Some(git) = self.stored(&a.repo_id).await? else {
471 return Ok(None);
472 };
473 Ok(git
474 .read_file(&a.git_ref, &a.path)
475 .await?
476 .filter(|bytes| bytes.len() <= a.max_bytes as usize)
477 .map(|bytes| RawFile { size: bytes.len() as u64, data: base64::engine::general_purpose::STANDARD.encode(bytes) }))
478 }
479
480 async fn raw_blobs(&self, a: RawBlobsArgs) -> Result<Vec<RawBlob>> {
481 use base64::Engine;
482 let Some(git) = self.stored(&a.repo_id).await? else {
483 return Ok(Vec::new());
484 };
485 let hashes: Vec<&String> = a.hashes.iter().take(MAX_READ_BLOBS).collect();
486 let mut out = Vec::with_capacity(hashes.len());
487 // A few at a time, as listing::read does: each is a round trip.
488 for group in hashes.chunks(8) {
489 let read = futures_util::future::try_join_all(group.iter().map(|hash| git.read_blob(hash))).await?;
490 for (hash, bytes) in group.iter().zip(read) {
491 let size = bytes.as_ref().map_or(0, |bytes| bytes.len() as u64);
492 let data = bytes
493 .filter(|bytes| bytes.len() <= a.max_bytes as usize)
494 .map(|bytes| base64::engine::general_purpose::STANDARD.encode(bytes));
495 out.push(RawBlob { hash: (*hash).clone(), size, data });
496 }
497 }
498 Ok(out)
499 }
500
Prices are what g1t pays plus 20%, from the first second501 async fn read_blobs(&self, a: ReadBlobsArgs) -> Result<Vec<BlobText>> {
502 let Some(git) = self.stored(&a.repo_id).await? else {
503 return Ok(Vec::new());
504 };
505 listing::read(&git, &a.hashes, a.max_bytes.min(MAX_TEXT_BYTES as u32)).await
506 }
507
508 async fn create(&self, a: CreateArgs) -> Result<Outcome<Repo>> {
509 if !a.owner.verified {
510 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
511 }
512 let name = a.name.trim().to_lowercase();
513 if !is_valid_repo_name(&name) {
514 return Ok(Outcome::fail(
515 FailureCode::Invalid,
516 "Use letters, digits, dots, hyphens and underscores only.",
517 ));
518 }
519 let namespace = a.namespace.trim().to_lowercase();
520 if namespace.is_empty() {
521 return Ok(Outcome::fail(
522 FailureCode::Invalid,
523 "Say which workspace to create the repository in.",
524 ));
525 }
Merge main (membership, two-factor, GitHub repo roles) into tokens526 let Some(role) = a.owner.role_in(&namespace) else {
Prices are what g1t pays plus 20%, from the first second527 return Ok(Outcome::fail(
528 FailureCode::Forbidden,
529 "You are not a member of that workspace.",
530 ));
Merge main (membership, two-factor, GitHub repo roles) into tokens531 };
532 // Who may create which: the workspace's member privileges. A
533 // workspace's own token acts as an owner would.
534 let role = if a.owner.kind == PrincipalKind::Workspace { Role::Owner } else { role };
535 if let Some(why) = a.owner.privileges_in(&namespace).creation_refusal(role, a.is_private, &namespace) {
536 return Ok(Outcome::fail(FailureCode::Forbidden, why));
Prices are what g1t pays plus 20%, from the first second537 }
538 let path = RepoPath { namespace, name };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look539 match self.registry.by_path_any(&path).await? {
540 Some((_, None)) => {
541 return Ok(Outcome::fail(
542 FailureCode::Conflict,
543 "That workspace already has a repository with that name.",
544 ));
545 }
546 Some((_, Some(_))) => {
547 return Ok(Outcome::fail(
548 FailureCode::Conflict,
549 format!(
550 "{}/{} 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.",
551 path.namespace, path.name
552 ),
553 ));
554 }
555 None => {}
556 }
557 // With a credential (a GitHub App installation's token), everything
558 // is copied: every branch and tag. See mirror.rs.
559 let mut credentialed = None;
560 if let (Some(url), Some(token)) = (a.import_url.as_deref(), a.import_token.as_deref()) {
561 let Some(url) = import::clean_url(url) else {
562 return Ok(Outcome::fail(FailureCode::Invalid, "That is not an https repository address."));
563 };
564 let source = mirror::Endpoint::github(&url, token);
565 match mirror::probe(&source).await? {
566 Ok(advertised) => credentialed = Some((source, advertised)),
567 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
568 }
Prices are what g1t pays plus 20%, from the first second569 }
570 // An import is fetched before anything is created, so that an
571 // address that does not work leaves nothing behind.
572 let mut imported = None;
573 if let Some(url) = a
574 .import_url
575 .as_deref()
576 .map(str::trim)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look577 .filter(|url| !url.is_empty() && credentialed.is_none())
Prices are what g1t pays plus 20%, from the first second578 {
579 let Some(url) = import::clean_url(url) else {
580 return Ok(Outcome::fail(
581 FailureCode::Invalid,
582 "Give the https address of a public repository, such as https://github.com/owner/repo.",
583 ));
584 };
585 let remote = match import::discover(&url).await? {
586 Ok(remote) => remote,
587 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
588 };
A public import copies every branch and tag, so an imported library keeps its releases589 imported = Some((remote, url));
Prices are what g1t pays plus 20%, from the first second590 }
591 let now = now_ms();
592 let repo = Repo {
593 id: new_id("rep", now),
594 namespace: path.namespace,
595 name: path.name,
596 description: a
597 .description
598 .map(|text| text.trim().to_owned())
599 .filter(|text| !text.is_empty()),
600 is_private: a.is_private,
601 owner_id: a.owner.id.clone(),
602 default_branch: imported
603 .as_ref()
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look604 .map(|(remote, _)| remote.branch.clone())
605 .or_else(|| credentialed.as_ref().and_then(|(_, advertised)| advertised.default_branch()))
606 .unwrap_or_else(|| "main".to_owned()),
Prices are what g1t pays plus 20%, from the first second607 fork_of: None,
608 protected: false,
609 created_at: rfc3339(now),
610 topics: Vec::new(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look611 website: None,
612 archived_at: None,
Merge branch 'mirroring' into artifacts-mode613 mirror: a.mirror.clone(),
Prices are what g1t pays plus 20%, from the first second614 };
Merge branch 'worktree-agent-a2013627e5ea4ab13'615 let namespace = match self.place(&repo).await? {
616 Ok(namespace) => namespace,
617 Err(unplaced) => return Ok(Outcome::fail(FailureCode::Conflict, unplaced.message())),
618 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily619 self.registry
620 .claim_store_key(&repo, namespace.as_deref(), &self.store.default_namespace())
621 .await?;
Prices are what g1t pays plus 20%, from the first second622 self.store
623 .create(
624 &store_key(&repo),
625 repo.description.as_deref(),
626 &repo.default_branch,
627 )
628 .await?;
629 self.registry.insert(&repo).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look630 // A repository that was transferred away from this path stops
631 // redirecting here.
632 self.registry
633 .drop_redirect(&RepoPath {
634 namespace: repo.namespace.clone(),
635 name: repo.name.clone(),
636 })
637 .await?;
A public import copies every branch and tag, so an imported library keeps its releases638 // Every branch and tag the import made, announced as pushes.
639 let mut pushed: Vec<(String, String)> = Vec::new();
640 // A public repository, read with no credential: every branch and
641 // tag is copied too, the default branch the one its HEAD names.
642 if let Some((_, url)) = imported {
Prices are what g1t pays plus 20%, from the first second643 let access = self
644 .store
645 .open(&store_key(&repo))
646 .await?
647 .access(Scope::Write)
648 .await?;
A public import copies every branch and tag, so an imported library keeps its releases649 let target = mirror::Endpoint::bearer(&access.remote, &access.token);
650 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 200ms651 self.refs_moved(&repo.id).await;
A public import copies every branch and tag, so an imported library keeps its releases652 match copied {
653 Ok(copied) => pushed = mirror::import_pushes(&copied.updated, &repo.default_branch),
654 Err(reason) => {
655 self.registry.remove(&repo.id).await?;
656 return Ok(Outcome::fail(
657 FailureCode::Invalid,
658 format!("The repository could not be stored: {reason}"),
659 ));
660 }
Prices are what g1t pays plus 20%, from the first second661 }
662 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look663 if let Some((source, _)) = credentialed {
664 let access = self
665 .store
666 .open(&store_key(&repo))
667 .await?
668 .access(Scope::Write)
669 .await?;
670 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 200ms671 let copied = mirror::copy(&source, &target, mirror::Prune::Yes).await?;
672 self.refs_moved(&repo.id).await;
673 match copied {
A public import copies every branch and tag, so an imported library keeps its releases674 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 look675 Err(reason) => {
676 self.registry.remove(&repo.id).await?;
677 return Ok(Outcome::fail(
678 FailureCode::Invalid,
679 format!("The repository could not be copied: {reason}"),
680 ));
681 }
682 }
683 }
Merge main (membership, two-factor, GitHub repo roles) into tokens684 // Whoever creates a repository is an Admin of it, as a role given
685 // to them on it, whatever the workspace's base permission.
686 if a.owner.kind == PrincipalKind::User
687 && let Some(identity) = &self.identity
688 {
689 let granted: Result<bool> = g1t_kit::call(
690 identity,
691 "grant_creator",
692 &g1t_contracts::members::GrantCreatorArgs {
693 repo_id: repo.id.clone(),
694 namespace: repo.namespace.clone(),
695 name: repo.name.clone(),
696 user_id: a.owner.id.clone(),
697 },
698 )
699 .await;
700 if let Err(error) = granted {
701 worker::console_error!("creator of {} not given Admin: {error}", repo.id);
702 }
703 }
Prices are what g1t pays plus 20%, from the first second704 self.publish(NewEvent {
705 kind: "repo.created",
706 source: SOURCE,
707 repo_id: Some(repo.id.clone()),
708 actor: Some(a.owner.id),
709 data: RepoCreated {
710 repo_id: repo.id.clone(),
711 namespace: repo.namespace.clone(),
712 name: repo.name.clone(),
713 is_private: repo.is_private,
714 },
715 })
716 .await?;
A public import copies every branch and tag, so an imported library keeps its releases717 for (git_ref, head) in &pushed {
Merge branch 'mirroring' into artifacts-mode718 if repo.mirror.is_some() {
719 self.publish_mirrored_push(&repo, git_ref, None, head).await?;
720 } else {
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.721 self.publish_push(&repo, git_ref, None, head, None, None).await?;
Merge branch 'mirroring' into artifacts-mode722 }
Prices are what g1t pays plus 20%, from the first second723 }
724 Ok(Outcome::Ok(repo))
725 }
726
Merge branch 'worktree-agent-a2013627e5ea4ab13'727 /// Where a workspace keeps its data, asked of identity only when an EU
728 /// namespace is configured: without one, every workspace's
729 /// repositories go anywhere and identity is never asked.
730 async fn residency_of(&self, workspace: &str) -> Result<shards::Residency> {
731 if self.placement.eu.is_none() {
732 return Ok(shards::Residency::Anywhere);
733 }
734 let Some(identity) = &self.identity else {
735 return Ok(shards::Residency::Anywhere);
736 };
737 let residency: Option<g1t_contracts::identity::DataResidency> = g1t_kit::call(
738 identity,
739 "workspace_residency",
740 &g1t_contracts::identity::SlugArgs { slug: workspace.to_owned() },
741 )
742 .await?;
743 Ok(match residency {
744 Some(g1t_contracts::identity::DataResidency::Eu) => shards::Residency::Eu,
745 _ => shards::Residency::Anywhere,
746 })
747 }
748
749 /// How each bound namespace stands (namespaces.rs).
750 async fn standings(&self) -> Result<Vec<namespaces::Standing>> {
751 let bound = self.store.namespaces();
752 let default = self.store.default_namespace();
753 let now = now_ms();
754 let config = namespaces::Configured {
755 bound: &bound,
756 default: &default,
757 placement: &self.placement,
758 limits: &self.limits,
759 on_fallback: &|namespace| self.store.on_fallback(&shards::compose(Some(namespace), "x", &default)),
760 writable: &|namespace| self.store.writable(namespace),
761 breaker_open: &|namespace| resilience::open_now(namespace, now),
762 };
763 let (held, recent) = futures_util::future::join(namespaces::held(&self.registry.db), namespaces::recent(&self.registry.db, now)).await;
764 Ok(namespaces::standings(&config, &held?, &recent?))
765 }
766
767 /// The namespace a new repository goes in (shards.rs): its workspace's
768 /// residency, then how each namespace stands, read only when there is
769 /// a choice to make. `Ok(None)` for the default.
770 async fn place(&self, repo: &Repo) -> Result<std::result::Result<Option<String>, shards::Unplaced>> {
771 let residency = self.residency_of(&repo.namespace).await?;
772 let bound = self.store.namespaces();
773 let loads = if residency == shards::Residency::Anywhere && !self.placement.needs_loads(&bound) {
774 // One namespace to choose from at most: nothing to read.
775 bound
776 .iter()
777 .map(|namespace| shards::Load {
778 namespace: namespace.clone(),
779 bound: true,
780 writable: self.store.writable(namespace),
781 ..shards::Load::default()
782 })
783 .collect()
784 } else {
785 let default = self.store.default_namespace();
786 let now = now_ms();
787 let config = namespaces::Configured {
788 bound: &bound,
789 default: &default,
790 placement: &self.placement,
791 limits: &self.limits,
792 on_fallback: &|namespace| self.store.on_fallback(&shards::compose(Some(namespace), "x", &default)),
793 writable: &|namespace| self.store.writable(namespace),
794 breaker_open: &|namespace| resilience::open_now(namespace, now),
795 };
796 namespaces::loads(&self.registry.db, &config, now).await?
797 };
798 Ok(self.placement.choose(&repo.id, residency, &loads))
799 }
800
801 /// `storage_options`: what a workspace may choose about where its
802 /// repositories are kept.
803 fn storage_options(&self) -> StorageOptions {
804 let bound = self.store.namespaces();
805 StorageOptions { eu_available: self.placement.eu_available(&bound, |namespace| self.store.writable(namespace)) }
806 }
807
Prices are what g1t pays plus 20%, from the first second808 async fn tree(&self, a: TreeArgs) -> Result<Outcome<TreeView>> {
809 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
810 return Ok(not_found());
811 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily812 let git = self.read_git(&repo).await?;
Prices are what g1t pays plus 20%, from the first second813 let git_ref = a
814 .git_ref
815 .clone()
816 .unwrap_or_else(|| repo.default_branch.clone());
817
818 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
819 // An unknown ref is an error; a repo with no commits is just empty.
820 if a.git_ref.is_some() {
821 return Ok(Outcome::fail(
822 FailureCode::NotFound,
823 "No such branch, tag or commit.",
824 ));
825 }
826 return Ok(Outcome::Ok(TreeView {
827 repo,
828 git_ref,
829 path: a.tree_path,
830 head: None,
831 entries: Vec::new(),
832 readme: None,
833 }));
834 };
835
836 let no_directory = || Outcome::fail(FailureCode::NotFound, "No such directory.");
837 let mut entries = git.read_tree(&head.tree_hash).await?;
838 for segment in a.tree_path.split('/').filter(|segment| !segment.is_empty()) {
839 let next = entries.as_ref().and_then(|entries| {
840 entries
841 .iter()
842 .find(|entry| entry.name == segment && entry.kind == EntryKind::Tree)
843 });
844 let Some(next) = next else {
845 return Ok(no_directory());
846 };
847 entries = git.read_tree(&next.hash).await?;
848 }
849 let Some(mut entries) = entries else {
850 return Ok(no_directory());
851 };
852 // Directories first, then by name.
853 entries.sort_by(|a, b| {
854 (b.kind == EntryKind::Tree)
855 .cmp(&(a.kind == EntryKind::Tree))
856 .then_with(|| a.name.cmp(&b.name))
857 });
858
859 let readme_entry = entries
860 .iter()
861 .find(|entry| entry.kind == EntryKind::Blob && is_readme(&entry.name));
862 let readme = match readme_entry {
863 Some(entry) => git.read_blob(&entry.hash).await?.map(|bytes| Readme {
864 name: entry.name.clone(),
865 text: text_of(bytes),
866 }),
867 None => None,
868 };
869 Ok(Outcome::Ok(TreeView {
870 repo,
871 git_ref,
872 path: a.tree_path,
873 head: Some(head),
874 entries,
875 readme,
876 }))
877 }
878
879 async fn blob(&self, a: BlobArgs) -> Result<Outcome<BlobView>> {
880 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
881 return Ok(not_found());
882 };
883 let bytes = if a.file_path.is_empty() {
884 None
885 } else {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily886 let git = self.read_git(&repo).await?;
Prices are what g1t pays plus 20%, from the first second887 git.read_file(&a.git_ref, &a.file_path).await?
888 };
889 let Some(bytes) = bytes else {
890 return Ok(Outcome::fail(FailureCode::NotFound, "No such file."));
891 };
892 Ok(Outcome::Ok(BlobView {
893 repo,
894 git_ref: a.git_ref,
895 path: a.file_path,
896 size: bytes.len() as u64,
897 text: text_of(bytes),
898 }))
899 }
900
901 async fn blame(&self, a: BlameArgs) -> Result<Outcome<Blame>> {
902 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
903 return Ok(not_found());
904 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily905 let git = self.read_git(&repo).await?;
Prices are what g1t pays plus 20%, from the first second906 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
907 Ok(match blame::blame(&git, &git_ref, &a.file_path).await? {
908 Some(blame) => Outcome::Ok(blame),
909 None => not_found(),
910 })
911 }
912
913 async fn log(&self, a: LogArgs) -> Result<Outcome<Vec<Commit>>> {
914 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
915 return Ok(not_found());
916 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily917 let git = self.read_git(&repo).await?;
Prices are what g1t pays plus 20%, from the first second918 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
919 Ok(Outcome::Ok(git.log(&git_ref, a.limit).await?))
920 }
921
Merge last commits over the whole history: progress kept per ref and path, a push reads only its own commits922 /// Which commit last changed each entry of a directory. A finished
923 /// answer is kept in this colo's cache by repository, head commit and
924 /// path: a commit's history never changes, so it is good for as long as
925 /// it is kept. Every walk also keeps its progress by ref and path
926 /// (last_commits.rs), so the next call goes on from it: from where it
927 /// stopped, or for a new head, only back to the old one.
Branches and Tags pages, each file's last commit, and the branch menu on files928 async fn last_commits(&self, a: g1t_contracts::repos::LastCommitsArgs) -> Result<Outcome<g1t_contracts::repos::LastCommits>> {
929 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
930 return Ok(not_found());
931 };
932 let git = self.read_git(&repo).await?;
933 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
934 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
935 return Ok(Outcome::fail(FailureCode::NotFound, "No such branch, tag or commit."));
936 };
Merge last commits over the whole history: progress kept per ref and path, a push reads only its own commits937 // v2: v1 kept walks cut short at 300 commits as if finished.
Branches and Tags pages, each file's last commit, and the branch menu on files938 let key = format!(
Merge last commits over the whole history: progress kept per ref and path, a push reads only its own commits939 "https://last-commits.g1t.internal/v2/{}/{}/{}",
Branches and Tags pages, each file's last commit, and the branch menu on files940 repo.id,
941 head.hash,
942 a.tree_path.split('/').map(urlencoding_segment).collect::<Vec<_>>().join("/")
943 );
944 let cache = worker::Cache::default();
945 if let Ok(Some(mut kept)) = cache.get(key.as_str(), false).await {
946 if let Ok(found) = kept.json::<g1t_contracts::repos::LastCommits>().await {
947 return Ok(Outcome::Ok(found));
948 }
949 }
Merge last commits over the whole history: progress kept per ref and path, a push reads only its own commits950 let memo = last_commits::Memo::new(&repo.id, &git_ref, &a.tree_path);
951 let shared = self.shared.as_deref();
952 let progress = memo.get(shared).await;
953 // Asked with a budget, the walk stops past it; without one, past
954 // LAST_COMMITS_WALK_MS, well before the caller's waitUntil ends.
A last-commits walk answers within 2.5 s with what it has, and keeps only finished answers955 let started = worker::Date::now().as_millis();
Merge last commits over the whole history: progress kept per ref and path, a push reads only its own commits956 let budget = a.budget_ms.unwrap_or(LAST_COMMITS_WALK_MS);
957 let out_of_time = move || worker::Date::now().as_millis().saturating_sub(started) > budget;
958 let walk = last_commits::last_commits(&git, &head.hash, &a.tree_path, progress, &out_of_time, last_commits::MAX_READS).await?;
959 if walk.reads > 0 {
960 memo.keep(shared, &walk.progress).await;
961 }
962 let settled = walk.progress.settled();
963 let found = g1t_contracts::repos::LastCommits { complete: walk.progress.complete(), entries: walk.progress.found };
964 if !settled {
A last-commits walk answers within 2.5 s with what it has, and keeps only finished answers965 return Ok(Outcome::Ok(found));
966 }
Branches and Tags pages, each file's last commit, and the branch menu on files967 if let Ok(mut response) = worker::Response::from_json(&found) {
968 let _ = response.headers_mut().set("cache-control", "max-age=604800");
969 let _ = cache.put(key.as_str(), response).await;
970 }
971 Ok(Outcome::Ok(found))
972 }
973
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25974 /// How far each branch head has moved from the default branch's head,
Merge branch drift: count across merges the way git does; v2 cache key975 /// in one call (drift.rs). Each count is kept in this colo's cache by
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25976 /// repository and the pair of hashes, for good: neither history can
Merge branch drift: count across merges the way git does; v2 cache key977 /// change. A head that moved is the only one walked. A failed read is
978 /// not kept, and "too far to count" only for a day.
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25979 async fn branch_drift(&self, a: BranchDriftArgs) -> Result<Outcome<BranchDrifts>> {
980 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
981 return Ok(not_found());
982 };
983 if !store::is_commit_hash(&a.base) {
984 return Ok(Outcome::fail(FailureCode::Invalid, "The default branch's head is a full commit hash."));
985 }
986 let heads: Vec<String> = a.heads.into_iter().take(MAX_DRIFT_HEADS).collect();
987 let git = self.read_git(&repo).await?;
Merge branch drift: count across merges the way git does; v2 cache key988 // v2: answers kept before 2026-10-09 said "no count" for every
989 // branch whose default branch took a merge since it left (drift.rs).
990 let key = |head: &str| format!("https://drift.g1t.internal/v2/{}/{}/{head}", repo.id, a.base);
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25991 let cache = worker::Cache::default();
992 let (base, kept) = futures_util::future::join(
993 git.log(&a.base, 1),
994 futures_util::future::join_all(heads.iter().map(|head| {
995 let (cache, url) = (&cache, key(head));
996 async move {
997 if !store::is_commit_hash(head) {
998 return None;
999 }
1000 let mut found = cache.get(url.as_str(), false).await.ok()??;
1001 found.json::<BranchDrift>().await.ok()
1002 }
1003 })),
1004 )
1005 .await;
1006 let missing: Vec<String> = heads
1007 .iter()
1008 .zip(&kept)
1009 .filter(|(head, kept)| kept.is_none() && store::is_commit_hash(head))
1010 .map(|(head, _)| head.clone())
1011 .collect();
1012 let measured = drift::measure(&git, &a.base, &missing).await;
1013 let mut fresh: HashMap<String, BranchDrift> = HashMap::new();
1014 for (head, found) in missing.into_iter().zip(measured) {
1015 let answer = BranchDrift { head: head.clone(), commit: found.commit, drift: found.drift };
1016 if found.settled
1017 && let Ok(mut response) = worker::Response::from_json(&answer)
1018 {
Merge branch drift: count across merges the way git does; v2 cache key1019 // A count is kept for good; "too far to count" for a day, so
1020 // a change to how far a walk goes reaches it.
1021 let max_age = if answer.drift.is_some() { "public, max-age=31536000, immutable" } else { "public, max-age=86400" };
1022 let _ = response.headers_mut().set("cache-control", max_age);
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 251023 let _ = cache.put(key(&head).as_str(), response).await;
1024 }
1025 fresh.insert(head, answer);
1026 }
1027 let branches = heads
1028 .iter()
1029 .zip(kept)
1030 .map(|(head, kept)| {
1031 kept.or_else(|| fresh.get(head).cloned())
1032 .unwrap_or_else(|| BranchDrift { head: head.clone(), commit: None, drift: None })
1033 })
1034 .collect();
1035 Ok(Outcome::Ok(BranchDrifts { base: base.ok().and_then(|log| log.into_iter().next()), branches }))
1036 }
1037
Branches and Tags pages, each file's last commit, and the branch menu on files1038 /// The repository's tags, newest commit first, at most 100.
1039 async fn tags(&self, a: g1t_contracts::repos::TagsArgs) -> Result<Outcome<Vec<g1t_contracts::repos::Tag>>> {
1040 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
1041 return Ok(not_found());
1042 };
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 251043 // Kept until the refs move, as the branch list is (store.rs): listing
1044 // the refs is a round trip to the store on every call otherwise.
1045 let version = refs_cache::usable(registry::refs_state(&repo.id), now_ms()).filter(|_| !self.store.on_fallback(&store_key(&repo)));
1046 let kept_at = version.map(|version| format!("https://tags.g1t.internal/{}/{version}", repo.id));
1047 if let Some(url) = &kept_at
1048 && let Ok(Some(mut kept)) = worker::Cache::default().get(url.as_str(), false).await
1049 && let Ok(tags) = kept.json::<Vec<g1t_contracts::repos::Tag>>().await
1050 {
1051 return Ok(Outcome::Ok(tags));
1052 }
1053 let (tags, complete) = self.read_tags(&repo).await?;
1054 if complete
1055 && let Some(url) = &kept_at
1056 && let Ok(mut response) = worker::Response::from_json(&tags)
1057 {
1058 let _ = response.headers_mut().set("cache-control", "public, max-age=300");
1059 let _ = worker::Cache::default().put(url.as_str(), response).await;
1060 }
1061 Ok(Outcome::Ok(tags))
1062 }
1063
1064 /// The tags, and whether every one's commit was read (only then kept).
1065 async fn read_tags(&self, repo: &Repo) -> Result<(Vec<g1t_contracts::repos::Tag>, bool)> {
1066 let git = self.store.open(&store_key(repo)).await?;
Branches and Tags pages, each file's last commit, and the branch menu on files1067 let access = git.access(Scope::Read).await?;
1068 let named: Vec<(String, String)> = refs::heads_and_tags(refs::all(&access).await?)
1069 .into_iter()
1070 .filter_map(|(name, hash)| name.strip_prefix("refs/tags/").map(|tag| (tag.to_owned(), hash)))
1071 .collect();
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 251072 let read = self.read_git(repo).await?;
Branches and Tags pages, each file's last commit, and the branch menu on files1073 let commits = futures_util::future::join_all(named.iter().take(MAX_TAGS_READ).map(|(_, hash)| read.log(hash, 1))).await;
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 251074 let complete = commits.iter().all(Result::is_ok);
Branches and Tags pages, each file's last commit, and the branch menu on files1075 let mut tags: Vec<g1t_contracts::repos::Tag> = named
1076 .into_iter()
1077 .zip(commits.into_iter().map(|found| found.ok().and_then(|list| list.into_iter().next())).chain(std::iter::repeat(None)))
1078 .map(|((name, _), commit)| g1t_contracts::repos::Tag { name, commit })
1079 .collect();
1080 tags.sort_by(|a, b| {
1081 let at = |tag: &g1t_contracts::repos::Tag| tag.commit.as_ref().map(|c| c.authored_at.clone()).unwrap_or_default();
1082 at(b).cmp(&at(a)).then_with(|| b.name.cmp(&a.name))
1083 });
1084 tags.truncate(MAX_TAGS_READ);
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 251085 Ok((tags, complete))
Branches and Tags pages, each file's last commit, and the branch menu on files1086 }
1087
Prices are what g1t pays plus 20%, from the first second1088 /// The repository's branches, default branch first.
1089 async fn branches(&self, a: BranchesArgs) -> Result<Outcome<Vec<Branch>>> {
1090 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
1091 return Ok(not_found());
1092 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1093 let mut branches = self.read_git(&repo).await?.branches().await?;
Prices are what g1t pays plus 20%, from the first second1094 branches.sort_by_key(|branch| branch.name != repo.default_branch);
1095 Ok(Outcome::Ok(branches))
1096 }
1097
1098 /// Whether a pull request's source lacks commits that the branch it
1099 /// would merge into has.
1100 async fn behind(&self, a: BehindArgs) -> Result<bool> {
1101 let Some(source) = self.registry.by_id(&a.source_id).await? else {
1102 return Ok(false);
1103 };
1104 let target = match &source.fork_of {
1105 Some(id) => self.registry.by_id(id).await?,
1106 None => Some(source.clone()),
1107 };
1108 let Some(target) = target else {
1109 return Ok(false);
1110 };
1111 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 bar1112 let target_branch = a.target_branch.unwrap_or_else(|| target.default_branch.clone());
Prices are what g1t pays plus 20%, from the first second1113 let target_head = self
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1114 .read_git(&target)
Prices are what g1t pays plus 20%, from the first second1115 .await?
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1116 .log(&target_branch, 1)
Prices are what g1t pays plus 20%, from the first second1117 .await?
1118 .into_iter()
1119 .next()
1120 .map(|commit| commit.hash);
1121 let Some(target_head) = target_head else {
1122 return Ok(false);
1123 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1124 let source_git = self.read_git(&source).await?;
Prices are what g1t pays plus 20%, from the first second1125 let history = source_git.log(&branch, MAX_ANCESTRY).await?;
1126 if history.is_empty() {
1127 return Ok(false);
1128 }
1129 Ok(!descends_from(&source_git, &history, &target_head).await?)
1130 }
1131
1132 /// The files a pull request's source and the default branch it would
1133 /// merge into each changed since they last agreed. Where the two lists
1134 /// share no file, the merge cannot conflict; where they do, it may.
1135 async fn divergence(&self, a: BehindArgs) -> Result<Option<Divergence>> {
1136 let Some(source) = self.registry.by_id(&a.source_id).await? else {
1137 return Ok(None);
1138 };
1139 let target = match &source.fork_of {
1140 Some(id) => self.registry.by_id(id).await?,
1141 None => Some(source.clone()),
1142 };
1143 let Some(target) = target else {
1144 return Ok(None);
1145 };
1146 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 bar1147 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 daily1148 let source_git = self.read_git(&source).await?;
1149 let target_git = self.read_git(&target).await?;
1150 // The target's side is the same for every pull request into it, and
1151 // worked out once per head (coalesce.rs).
1152 let (history, side) = futures_util::future::try_join(
Prices are what g1t pays plus 20%, from the first second1153 source_git.log(&branch, MAX_ANCESTRY),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1154 self.target_side(&target, &target_git, &target_branch),
Prices are what g1t pays plus 20%, from the first second1155 )
1156 .await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1157 let target_history = &side.history;
Prices are what g1t pays plus 20%, from the first second1158 let (Some(head), Some(base)) = (history.first(), target_history.first()) else {
1159 return Ok(None);
1160 };
1161 let behind = !descends_from(&source_git, &history, &base.hash).await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1162 let merge_base = nearest_ancestor_in(&source_git, &history, &side.shared).await?;
Prices are what g1t pays plus 20%, from the first second1163 let mut divergence = Divergence {
1164 head: head.hash.clone(),
1165 base: base.hash.clone(),
1166 merge_base: merge_base.clone(),
1167 behind,
1168 ..Divergence::default()
1169 };
1170 let merge_base_tree = match &merge_base {
1171 Some(hash) => target_history
1172 .iter()
1173 .find(|commit| commit.hash == *hash)
1174 .map(|commit| commit.tree_hash.clone()),
1175 None => None,
1176 };
1177 let Some(merge_base_tree) = merge_base_tree else {
1178 // No common history to compare from: say nothing is known.
1179 divergence.truncated = true;
1180 return Ok(Some(divergence));
1181 };
1182 let (ours, truncated_ours) =
1183 diff::changed_paths(&source_git, Some(&merge_base_tree), &head.tree_hash).await?;
1184 divergence.ours = ours;
1185 divergence.truncated = truncated_ours;
1186 if behind {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1187 let now = now_ms();
1188 let key = (target.id.clone(), merge_base_tree.clone(), base.tree_hash.clone());
1189 let (theirs, truncated_theirs) = match THEIRS.with(|memo| memo.borrow().get(&key, now)) {
1190 Some(kept) => kept,
1191 None => {
1192 let found = diff::changed_paths(&target_git, Some(&merge_base_tree), &base.tree_hash).await?;
1193 THEIRS.with(|memo| memo.borrow_mut().put(key, found.clone(), now));
1194 found
1195 }
1196 };
Prices are what g1t pays plus 20%, from the first second1197 divergence.theirs = theirs;
1198 divergence.truncated |= truncated_theirs;
1199 }
1200 Ok(Some(divergence))
1201 }
1202
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1203 /// A target branch's history from its head, worked out once per head
1204 /// for every pull request asking about it (coalesce.rs). The head is
1205 /// read under the refs version; the history by its hash, which the
1206 /// object cache keeps for good.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1207 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 daily1208 let now = now_ms();
1209 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 bar1210 .map(|version| (target.id.clone(), branch.to_owned(), version));
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1211 if let Some(key) = &key
1212 && let Some(side) = TARGETS.with(|memo| memo.borrow().get(key, now))
1213 {
1214 return Ok(side);
1215 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1216 let history = match git.log(branch, 1).await?.first() {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1217 Some(head) => git.log(&head.hash, MAX_ANCESTRY).await?,
1218 None => Vec::new(),
1219 };
1220 let side = coalesce::TargetSide::new(history);
1221 if let Some(key) = key {
1222 TARGETS.with(|memo| memo.borrow_mut().put(key, side.clone(), now));
1223 }
1224 Ok(side)
1225 }
1226
Prices are what g1t pays plus 20%, from the first second1227 async fn head(&self, a: HeadArgs) -> Result<Option<String>> {
1228 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
1229 return Ok(None);
1230 };
1231 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 daily1232 let git = self.read_git(&repo).await?;
Prices are what g1t pays plus 20%, from the first second1233 Ok(git
1234 .log(branch, 1)
1235 .await?
1236 .into_iter()
1237 .next()
1238 .map(|commit| commit.hash))
1239 }
1240
1241 async fn delete_branch(&self, a: DeleteBranchArgs) -> Result<Outcome<bool>> {
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches1242 if !deletable_branch(&a.branch, a.head.as_deref()) {
Prices are what g1t pays plus 20%, from the first second1243 return Ok(Outcome::fail(
1244 FailureCode::Forbidden,
1245 "Only branches g1t made for itself can be deleted this way.",
1246 ));
1247 }
1248 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
1249 return Ok(not_found());
1250 };
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches1251 if a.branch == repo.default_branch {
1252 return Ok(Outcome::fail(FailureCode::Forbidden, "The default branch is never deleted."));
1253 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'1254 let repo = match self.unpaused(repo).await? {
1255 Ok(repo) => repo,
1256 Err((code, message)) => return Ok(Outcome::fail(code, message)),
1257 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1258 self.live(&repo).await?;
Prices are what g1t pays plus 20%, from the first second1259 let git = self.store.open(&store_key(&repo)).await?;
1260 let Some(old) = git
1261 .branches()
1262 .await?
1263 .into_iter()
1264 .find(|branch| branch.name == a.branch)
1265 .map(|branch| branch.hash)
1266 else {
1267 return Ok(Outcome::Ok(false));
1268 };
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches1269 // Moved since the caller looked: someone else's commits are on it.
1270 if a.head.as_deref().is_some_and(|head| head != old) {
1271 return Ok(Outcome::fail(FailureCode::Conflict, format!("{} moved, so it was left alone.", a.branch)));
1272 }
Prices are what g1t pays plus 20%, from the first second1273 let access = git.access(Scope::Write).await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1274 let deleted = land::delete_ref(&access, &a.branch, &old).await?;
1275 self.refs_moved(&repo.id).await;
1276 if let Err(reason) = deleted {
Prices are what g1t pays plus 20%, from the first second1277 return Ok(Outcome::fail(
1278 FailureCode::Conflict,
1279 format!("{} could not be deleted: {reason}", a.branch),
1280 ));
1281 }
1282 Ok(Outcome::Ok(true))
1283 }
1284
1285 async fn fork_for_pull(&self, a: ForkArgs) -> Result<Outcome<Repo>> {
1286 let viewer = Some(a.actor.clone());
1287 let Some(source) = self
1288 .registry
1289 .by_id(&a.source_id)
1290 .await?
1291 .filter(|repo| can_read(repo, &viewer))
1292 else {
1293 return Ok(not_found());
1294 };
Merge branch 'mirroring' into artifacts-mode1295 if let Some((code, message)) = lifecycle::read_only_refusal(&source) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1296 return Ok(Outcome::fail(code, message));
1297 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'1298 // Its working copy is made in its namespace: not while it moves.
1299 let source = match self.unpaused(source).await? {
1300 Ok(source) => source,
1301 Err((code, message)) => return Ok(Outcome::fail(code, message)),
1302 };
Prices are what g1t pays plus 20%, from the first second1303 let now = now_ms();
1304 let fork = Repo {
1305 id: new_id("rep", now),
1306 namespace: PULLS_NAMESPACE.to_owned(),
1307 name: a.pull_id.clone(),
1308 description: None,
1309 // A fork is exactly as visible as the repo it came from.
1310 is_private: source.is_private,
1311 owner_id: a.actor.id.clone(),
1312 default_branch: source.default_branch.clone(),
1313 fork_of: Some(source.id.clone()),
1314 protected: false,
1315 created_at: rfc3339(now),
1316 topics: Vec::new(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1317 website: None,
1318 archived_at: None,
Merge branch 'mirroring' into artifacts-mode1319 mirror: None,
Prices are what g1t pays plus 20%, from the first second1320 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1321 // Artifacts forks within a namespace: the copy goes where its
1322 // repository is.
1323 let (namespace, _) = store::locate(&store_key(&source));
1324 self.registry
1325 .claim_store_key(&fork, Some(&namespace), &self.store.default_namespace())
1326 .await?;
Prices are what g1t pays plus 20%, from the first second1327 self.store
1328 .open(&store_key(&source))
1329 .await?
1330 .fork(&store_key(&fork))
1331 .await?;
1332 self.registry.insert(&fork).await?;
1333 self.publish(NewEvent {
1334 kind: "repo.forked",
1335 source: SOURCE,
1336 repo_id: Some(source.id.clone()),
1337 actor: Some(a.actor.id),
1338 data: RepoForked {
1339 repo_id: fork.id.clone(),
1340 source_repo_id: source.id,
1341 pull_id: a.pull_id,
1342 },
1343 })
1344 .await?;
1345 Ok(Outcome::Ok(fork))
1346 }
1347
1348 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 200ms1349 let found = self.registry.by_path(&a.path).await?;
1350 Ok(match self.authorize_git(&a.path, &a.viewer, a.service, found).await? {
1351 Outcome::Ok(repo) => {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1352 self.live(&repo).await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1353 let write = a.service == GitService::ReceivePack;
1354 if write {
1355 // A push with this credential would not pass through
1356 // here, so nothing that lists the refs is kept until it
1357 // has expired (see refs_cache.rs).
1358 let until = now_ms() + store::CREDENTIAL_LIFE_MS + 60_000;
1359 if let Err(error) = self.registry.refs_open(&repo.id, until).await {
1360 // Before the column exists nothing is kept anyway.
1361 if registry::refs_state(&repo.id).is_some() {
1362 return Err(error);
1363 }
1364 }
1365 }
1366 let scope = if write { Scope::Write } else { Scope::Read };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1367 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 200ms1368 }
1369 Outcome::Fail(failure) => Outcome::Fail(failure),
1370 })
1371 }
1372
1373 /// The repository at `path` (`found`, as just read), if the viewer may
1374 /// use `service` on it: fetch from it, or push to it. A push to a path
1375 /// with nothing there makes the repository, in a workspace the pusher
1376 /// belongs to.
1377 async fn authorize_git(
1378 &self,
1379 path: &RepoPath,
1380 viewer: &Viewer,
1381 service: GitService,
1382 found: Option<Repo>,
1383 ) -> Result<Outcome<Repo>> {
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1384 let mut a = GitAccessArgs {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1385 path: path.clone(),
1386 viewer: viewer.clone(),
1387 service,
1388 };
Prices are what g1t pays plus 20%, from the first second1389 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 step1390 // An access token: pushing needs code:write, reading a private
1391 // repository code:read. A public repository reads as it would for
1392 // anyone. Which repositories a token reaches is its owner's, checked
1393 // below as for anyone.
1394 if let Some(access) = a.viewer.as_ref().and_then(|user| user.token.as_deref()).cloned() {
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1395 // A workflow job's token, and a deploy key, reach their own
1396 // repository only; a job's also the working copies of that
1397 // repository's pull requests, where their heads are.
1398 let name = format!("{}/{}", path.namespace, path.name);
1399 let source = match found.as_ref().and_then(|repo| repo.fork_of.as_deref()) {
1400 Some(source_id) if g1t_contracts::scopes::decide_repo(&access, &name).is_some() => self
1401 .registry
1402 .by_id(source_id)
1403 .await?
1404 .map(|source| format!("{}/{}", source.namespace, source.name)),
1405 _ => None,
1406 };
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1407 let public = found.as_ref().is_some_and(|repo| !repo.is_private);
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1408 if let Some(why) = git_token_refusal(&access, &name, source.as_deref(), write, public, found.is_some()) {
1409 return Ok(Outcome::fail(FailureCode::Forbidden, format!("{why}\n")));
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1410 }
1411 if !write && !access.allows(g1t_contracts::scopes::Scope::CodeRead) {
1412 a.viewer = None;
1413 }
1414 }
1415
Prices are what g1t pays plus 20%, from the first second1416 // Anonymous callers are asked to authenticate whether or not the repo
1417 // exists, so private repos cannot be told apart from missing ones.
1418 let denied = || match &a.viewer {
1419 Some(_) => not_found(),
1420 None => Outcome::fail(FailureCode::Unauthenticated, "Authentication required."),
1421 };
1422 // An agent's token works through the API only: its sandbox has its
1423 // own way to push, to its own pull request.
1424 if a.viewer.as_ref().is_some_and(|user| user.kind == PrincipalKind::Agent) {
1425 return Ok(Outcome::fail(
1426 FailureCode::Forbidden,
1427 "A g1t agent's token cannot be used with git.",
1428 ));
1429 }
1430 if let (true, Some(user)) = (write, &a.viewer)
1431 && !user.verified
1432 {
1433 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
1434 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1435 let repo = match found {
Prices are what g1t pays plus 20%, from the first second1436 Some(repo) => {
1437 let allowed = if write {
1438 can_write(&repo, &a.viewer)
1439 } else {
1440 self.may_read(&repo, &a.viewer).await?
1441 };
1442 if !allowed {
1443 return Ok(denied());
1444 }
Merge branch 'mirroring' into artifacts-mode1445 // An archived repository, a mirror standing by, or a pull
1446 // request's copy of either, is read-only.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1447 if write {
1448 let archived = match &repo.fork_of {
1449 Some(source) => self.registry.by_id(source).await?,
1450 None => Some(repo.clone()),
1451 };
1452 match archived {
1453 Some(source) => {
Merge branch 'mirroring' into artifacts-mode1454 if let Some((code, message)) = lifecycle::read_only_refusal(&source) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1455 return Ok(Outcome::fail(code, format!("{message}\n")));
1456 }
1457 }
1458 // The repository it was copied from is deleted.
1459 None => return Ok(denied()),
1460 }
1461 }
Prices are what g1t pays plus 20%, from the first second1462 repo
1463 }
1464 None => {
1465 // Push to create, in a workspace the pusher belongs to.
1466 let owner = a
1467 .viewer
1468 .as_ref()
1469 .filter(|user| write && user.is_member(&a.path.namespace.to_lowercase()));
1470 let Some(owner) = owner else {
1471 return Ok(denied());
1472 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1473 let created = self.create(push_to_create(owner, &a.path)).await?;
Prices are what g1t pays plus 20%, from the first second1474 match created {
1475 Outcome::Ok(repo) => repo,
1476 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
1477 }
1478 }
1479 };
Merge branch 'worktree-agent-a2013627e5ea4ab13'1480 // A push, or a credential to push with, waits while the repository
1481 // moves between namespaces (moves.rs), and goes to where it is now.
1482 if write {
1483 return Ok(match self.unpaused(repo).await? {
1484 Ok(repo) => Outcome::Ok(repo),
1485 Err((code, message)) => Outcome::fail(code, format!("{message}\n")),
1486 });
1487 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1488 Ok(Outcome::Ok(repo))
Prices are what g1t pays plus 20%, from the first second1489 }
1490
1491 async fn land(&self, a: LandArgs) -> Result<Outcome<Landed>> {
1492 let actor: Viewer = Some(a.actor.clone());
1493 let Some(source) = self.registry.by_id(&a.source_id).await? else {
1494 return Ok(not_found());
1495 };
1496 // A fork lands on the repository it came from; a branch on its own.
1497 let target = match &source.fork_of {
1498 Some(id) => self.registry.by_id(id).await?,
1499 None => Some(source.clone()),
1500 };
1501 let Some(target) = target.filter(|repo| can_read(repo, &actor)) else {
1502 return Ok(not_found());
1503 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1504 if !registry::can(&target, &actor, Capability::Merge) {
Prices are what g1t pays plus 20%, from the first second1505 return Ok(Outcome::fail(
1506 FailureCode::Forbidden,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1507 access::needs(Capability::Merge, &format!("{}/{}", target.namespace, target.name)),
Prices are what g1t pays plus 20%, from the first second1508 ));
1509 }
1510 if !a.actor.verified {
1511 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
1512 }
Merge branch 'mirroring' into artifacts-mode1513 if let Some((code, message)) = lifecycle::read_only_refusal(&target) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1514 return Ok(Outcome::fail(code, message));
1515 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'1516 // Moving between namespaces: wait for it (moves.rs). Both are read
1517 // again once it is done, for their new keys.
1518 let (source, target) = match (self.unpaused(source).await?, self.unpaused(target).await?) {
1519 (Ok(source), Ok(target)) => (source, target),
1520 (Err((code, message)), _) | (_, Err((code, message))) => return Ok(Outcome::fail(code, message)),
1521 };
Prices are what g1t pays plus 20%, from the first second1522
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1523 let branch = &a.target_branch.clone().unwrap_or_else(|| target.default_branch.clone());
Prices are what g1t pays plus 20%, from the first second1524 let from_fork = source.id != target.id;
1525 let source_branch = match a.branch {
1526 Some(name) if !from_fork && name == *branch => {
1527 return Ok(Outcome::fail(
1528 FailureCode::Invalid,
1529 format!("{branch} cannot be merged into itself."),
1530 ));
1531 }
1532 Some(name) => name,
1533 None if from_fork => branch.clone(),
1534 None => {
1535 return Ok(Outcome::fail(
1536 FailureCode::Invalid,
1537 "Say which branch to merge.",
1538 ));
1539 }
1540 };
1541
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1542 self.live(&source).await?;
Prices are what g1t pays plus 20%, from the first second1543 let source_git = self.store.open(&store_key(&source)).await?;
1544 let target_git = self.store.open(&store_key(&target)).await?;
1545 let history = source_git.log(&source_branch, MAX_ANCESTRY).await?;
1546 let Some(new) = history.first().map(|commit| commit.hash.clone()) else {
1547 return Ok(Outcome::fail(
1548 FailureCode::Conflict,
1549 "This pull request has no commits to merge.",
1550 ));
1551 };
1552 let old = target_git
1553 .log(branch, 1)
1554 .await?
1555 .into_iter()
1556 .next()
1557 .map(|commit| commit.hash);
1558
1559 if old.as_deref() == Some(new.as_str()) {
1560 return Ok(Outcome::Ok(Landed {
1561 commit: new,
1562 previous: None,
1563 }));
1564 }
1565 // Moving the branch to a commit that does not descend from its
1566 // current head would discard whatever landed in between.
1567 if let Some(old) = &old
1568 && !descends_from(&source_git, &history, old).await?
1569 {
1570 let remedy = if from_fork {
1571 format!("Pull {branch} into the pull request's fork, push, and merge again.")
1572 } else {
1573 format!("Merge {branch} into {source_branch}, push, and merge again.")
1574 };
1575 return Ok(Outcome::fail(
1576 FailureCode::Conflict,
1577 format!("{branch} has moved since this pull request was opened. {remedy}"),
1578 ));
1579 }
1580
1581 // For a branch the objects are already in the target; sending them
1582 // again is harmless and keeps one way of moving a ref.
1583 let source_access = source_git.access(Scope::Read).await?;
1584 let target_access = target_git.access(Scope::Write).await?;
1585 let pushed =
1586 land::fast_forward(&source_access, &target_access, branch, old.as_deref(), &new)
1587 .await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1588 self.refs_moved(&target.id).await;
Prices are what g1t pays plus 20%, from the first second1589 if let Err(reason) = pushed {
1590 // Most often another pull request landed between the check and the push.
1591 return Ok(Outcome::fail(
1592 FailureCode::Conflict,
1593 format!("{branch} could not be updated: {reason}"),
1594 ));
1595 }
1596 self.publish_push(
1597 &target,
1598 &format!("refs/heads/{branch}"),
1599 old.as_deref(),
1600 &new,
Merge branch 'worktree-agent-a3abfcce648e87dca'1601 Some(&a.actor),
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.1602 None,
Prices are what g1t pays plus 20%, from the first second1603 )
1604 .await?;
1605 Ok(Outcome::Ok(Landed {
1606 commit: new,
1607 previous: old,
1608 }))
1609 }
1610
1611 async fn compare(&self, a: CompareArgs) -> Result<Outcome<Comparison>> {
1612 let Some(repo) = self
1613 .visible(self.registry.by_id(&a.repo_id).await?, &a.viewer)
1614 .await?
1615 else {
1616 return Ok(not_found());
1617 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1618 let git = self.read_git(&repo).await?;
Prices are what g1t pays plus 20%, from the first second1619 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 bar1620 // A pull request into another branch is compared from where it
1621 // left that branch.
1622 let base_branch = a.base_branch.clone();
Prices are what g1t pays plus 20%, from the first second1623 // The head's history is only searched when the base is worked out
1624 // from another branch.
1625 let depth = if a.base.is_some() || is_commit_hash(head_ref) { 1 } else { MAX_ANCESTRY };
1626 let history = git.log(head_ref, depth).await?;
1627 let Some(head) = history.first() else {
1628 return Ok(Outcome::fail(
1629 FailureCode::Conflict,
1630 "There are no commits to compare.",
1631 ));
1632 };
1633
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1634 // Where the head's history meets the default branch of `against`,
1635 // or the branch asked for.
Prices are what g1t pays plus 20%, from the first second1636 let shared_with = async |against: &Repo| -> Result<Option<String>> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1637 let against_git = self.read_git(against).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1638 let branch = base_branch.as_deref().unwrap_or(&against.default_branch);
Prices are what g1t pays plus 20%, from the first second1639 let shared: HashSet<String> = against_git
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1640 .log(branch, MAX_ANCESTRY)
Prices are what g1t pays plus 20%, from the first second1641 .await?
1642 .into_iter()
1643 .map(|commit| commit.hash)
1644 .collect();
1645 nearest_ancestor_in(&git, &history, &shared).await
1646 };
1647 let base = match (a.base, &repo.fork_of) {
1648 (Some(base), _) => Some(base),
1649 // A fork is compared with the last commit it shares with the
1650 // repository it came from.
1651 (None, Some(target_id)) => match self.registry.by_id(target_id).await? {
1652 Some(target) => shared_with(&target).await?,
1653 None => None,
1654 },
1655 // A branch, with the point where it left the default branch.
1656 // A single commit, with its first parent.
1657 (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 bar1658 (None, None) if head_ref != base_branch.as_deref().unwrap_or(&repo.default_branch) => {
1659 shared_with(&repo).await?
1660 }
Prices are what g1t pays plus 20%, from the first second1661 (None, None) => head.parents.first().cloned(),
1662 };
1663 let base_tree = match &base {
1664 Some(base) => git
1665 .log(base, 1)
1666 .await?
1667 .into_iter()
1668 .next()
1669 .map(|commit| commit.tree_hash),
1670 None => None,
1671 };
1672 let (files, truncated) =
1673 diff::compare_trees(&git, base_tree.as_deref(), &head.tree_hash).await?;
1674 Ok(Outcome::Ok(Comparison {
1675 base,
1676 head: head.hash.clone(),
1677 files,
1678 truncated,
1679 }))
1680 }
1681
Merge branch 'worktree-agent-a3abfcce648e87dca'1682 /// Reports that `git_ref` of `repo` (a full ref) now points to `after`,
1683 /// moved by `actor` (marked when that was a workflow job's token).
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.1684 /// `commits` is how many the move brought, when the caller knows: one
1685 /// for a file committed on the site or a branch brought up to date.
Prices are what g1t pays plus 20%, from the first second1686 async fn publish_push(
1687 &self,
1688 repo: &Repo,
1689 git_ref: &str,
1690 before: Option<&str>,
1691 after: &str,
Merge branch 'worktree-agent-a3abfcce648e87dca'1692 actor: Option<&User>,
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.1693 commits: Option<u32>,
Prices are what g1t pays plus 20%, from the first second1694 ) -> Result<()> {
Merge branch 'worktree-agent-a3abfcce648e87dca'1695 let caused_by_job = actor.and_then(g1t_contracts::events::job_run_of).map(str::to_owned);
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.1696 self.publish_git_push(repo, git_ref, before, after, actor.map(|user| user.id.clone()), false, caused_by_job, false, commits)
Merge branch 'mirroring' into artifacts-mode1697 .await
1698 }
1699
1700 /// A push copied in from the remote a mirror follows (mirror.rs), or
1701 /// made by filling a new mirror from it.
1702 async fn publish_mirrored_push(&self, repo: &Repo, git_ref: &str, before: Option<&str>, after: &str) -> Result<()> {
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.1703 self.publish_git_push(repo, git_ref, before, after, None, false, None, true, None).await
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1704 }
1705
1706 /// `publish_push`, saying whether the push reached the store without
1707 /// being scanned for secrets first.
Merge branch 'worktree-agent-a3abfcce648e87dca'1708 #[allow(clippy::too_many_arguments)]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1709 async fn publish_git_push(
1710 &self,
1711 repo: &Repo,
1712 git_ref: &str,
1713 before: Option<&str>,
1714 after: &str,
1715 actor: Option<String>,
1716 unscanned: bool,
Merge branch 'worktree-agent-a3abfcce648e87dca'1717 caused_by_job: Option<String>,
Merge branch 'mirroring' into artifacts-mode1718 mirrored: bool,
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.1719 commits: Option<u32>,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1720 ) -> Result<()> {
Prices are what g1t pays plus 20%, from the first second1721 self.publish(NewEvent {
1722 kind: "git.push",
1723 source: SOURCE,
1724 repo_id: Some(repo.id.clone()),
1725 actor,
1726 data: GitPush {
1727 repo_id: repo.id.clone(),
1728 git_ref: git_ref.to_owned(),
1729 before: before.map(str::to_owned),
1730 after: after.to_owned(),
1731 default_branch: git_ref.strip_prefix("refs/heads/")
1732 == Some(repo.default_branch.as_str()),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1733 unscanned,
Merge branch 'worktree-agent-a3abfcce648e87dca'1734 caused_by_job,
Merge branch 'mirroring' into artifacts-mode1735 mirrored,
1736 mirror: repo.mirror.clone(),
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.1737 commits,
Prices are what g1t pays plus 20%, from the first second1738 },
1739 })
1740 .await
1741 }
1742
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1743 /// Git over HTTPS. Only what decides the answer happens before it:
1744 /// the repository, who is asking and whether they may, the free
1745 /// workspace limits, push protection, and the store's own answer. The
1746 /// audit entry and what a push changed are recorded once git has its
1747 /// answer. Each answer says how long its steps took (`Server-Timing`).
1748 async fn git_http(&self, request: Request, env: &Env, ctx: &Context) -> Result<Response> {
1749 let mut timing = git_http::Timing::start();
Prices are what g1t pays plus 20%, from the first second1750 let Some(git) = git_http::parse(&request.url()?) else {
1751 return Response::error("Not found", 404);
1752 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1753 let response = match self.answer_git(request, &git, env, ctx, &mut timing).await {
1754 Ok(response) => response,
1755 // The git store is busy: git hears when to try again.
1756 Err(error) => match resilience::busy(&error.to_string()) {
1757 Some(busy) => git_http::busy_response(busy)?,
1758 None => return Err(error),
1759 },
1760 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1761 timing.apply(response)
1762 }
1763
1764 async fn answer_git(
1765 &self,
1766 request: Request,
1767 git: &git_http::GitRequest,
1768 env: &Env,
1769 ctx: &Context,
1770 timing: &mut git_http::Timing,
1771 ) -> Result<Response> {
1772 let write = git.service == GitService::ReceivePack;
1773 let get = request.method() == Method::Get;
1774 let identity = env.service("IDENTITY")?;
1775 // The repository and the caller's credentials, at once. A fetch may
1776 // go by the row as read a moment ago, for the same clone's next
1777 // request; a push always reads it. Anonymous callers cost nothing.
1778 let lookup = async {
1779 if write {
1780 self.registry.by_path(&git.path).await
1781 } else {
1782 self.registry.by_path_recent(&git.path).await
1783 }
1784 };
1785 let (found, viewer) =
1786 futures_util::future::join(lookup, git_http::viewer(&request, &identity)).await;
Merge branch 'worktree-agent-a8385d293d42c913a'1787 let mut found = found?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1788 timing.mark("repo");
Merge branch 'worktree-agent-a8385d293d42c913a'1789 // A workspace alias staff set (identity's aliases.rs: `g1t` for
1790 // `flagon-io`) is answered in place, as the repository under the
1791 // workspace's slug: pushes and some clients do not follow
1792 // redirects. Everything after this sees only the workspace's slug.
1793 let aliased = match found {
1794 Some(_) => None,
1795 None => git_http::aliased(git, &identity).await?,
1796 };
1797 if let Some(aliased) = &aliased {
1798 found = if write {
1799 self.registry.by_path(&aliased.path).await?
1800 } else {
1801 self.registry.by_path_recent(&aliased.path).await?
1802 };
1803 timing.mark("alias");
1804 }
1805 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 200ms1806 if found.is_none() {
1807 // A workspace that was renamed: git follows a redirect when it
1808 // first asks for refs, and uses the new address from then on.
1809 // A repository transferred to another workspace: the same, to
1810 // its new path. Fetches and pushes both follow either.
1811 let url = request.url()?;
1812 let (renamed, moved) = futures_util::future::join(
1813 git_http::renamed(&url, &identity),
1814 self.registry.resolve_moved(&git.path),
1815 )
1816 .await;
1817 timing.mark("moved");
1818 if let Some(location) = renamed? {
1819 return git_http::moved(&location, get);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1820 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1821 if let Some(now) = moved?
1822 && let Some(location) = git_http::transferred(&url, &now)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1823 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1824 return git_http::moved(&location, get);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1825 }
Prices are what g1t pays plus 20%, from the first second1826 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1827 let viewer = viewer?;
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)1828 // A person who has not confirmed their email address can do
1829 // nothing with git until they do: told so, not asked to sign in.
1830 if viewer.as_ref().is_some_and(g1t_contracts::User::awaits_confirmation) {
1831 let site = request.url()?.origin().ascii_serialization();
1832 return git_http::refuse(Outcome::<()>::fail(
1833 FailureCode::Forbidden,
1834 g1t_contracts::accounts::confirm_email_first(&site),
1835 ));
1836 }
Prices are what g1t pays plus 20%, from the first second1837 // A run credential is checked against its grants, then acts as the
1838 // 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 200ms1839 let (request, viewer, audit) = match self.admit_git(request, git, viewer, found.as_ref()).await? {
Prices are what g1t pays plus 20%, from the first second1840 run_access::Admitted::Go { request, viewer, entry } => (request, viewer, entry),
1841 run_access::Admitted::Refused(response) => return Ok(response),
1842 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1843 let mut after = AfterGit {
1844 audit,
1845 status: 0,
1846 message: None,
1847 push: None,
1848 };
1849 let repo = match self.authorize_git(&git.path, &viewer, git.service, found).await? {
1850 Outcome::Ok(repo) => repo,
Prices are what g1t pays plus 20%, from the first second1851 refused => {
1852 let response = git_http::refuse(refused)?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1853 after.ended(response.status_code(), None);
1854 after.spawn(env, ctx);
Prices are what g1t pays plus 20%, from the first second1855 return Ok(response);
1856 }
1857 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1858 // A pull request's working copy removed after it closed is made
1859 // again before git uses it (forks.rs).
1860 self.live(&repo).await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1861 timing.mark("access");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1862 // Clones check out the default branch g1t keeps, which can have
1863 // changed since the store made the repository.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1864 let default_branch = repo.fork_of.is_none().then(|| repo.default_branch.clone());
1865 let key = store_key(&repo);
1866 let scope = if write { Scope::Write } else { Scope::Read };
1867 let mut request = request;
1868 let protocol = refs_cache::protocol(request.headers().get("git-protocol")?.as_deref());
1869 // A fetch's POST is read here, to tell an `ls-refs` from a fetch of
1870 // objects; the store would have it read in full anyway.
1871 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 daily1872 // What it asks the store, for the meters (meters.rs).
1873 let call = git_ops::classify(git.service, git.endpoint, get, body.as_deref());
Merge branch 'worktree-agent-a2013627e5ea4ab13'1874 // Answers kept from the usual store may name refs the fallback
1875 // store does not have (fallback.rs): none are used, or kept.
1876 let fallback = self.store.on_fallback(&key);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1877 // An answer that lists refs may have been kept: see refs_cache.rs.
1878 let kept_key = refs_cache::kind(git, get, protocol, body.as_deref())
Merge branch 'worktree-agent-a2013627e5ea4ab13'1879 .filter(|_| !fallback)
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1880 .zip(refs_cache::usable(registry::refs_state(&repo.id), now_ms()))
1881 .map(|(kind, version)| {
1882 refs_cache::Key::new(&repo.id, version, default_branch.as_deref(), protocol, &kind)
1883 });
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1884 // A fresh clone's pack may have been kept too: see pack_cache.rs.
1885 // Under the same refs version, so never across a change to them.
1886 let pack_key = self
1887 .packs
1888 .as_ref()
Merge branch 'worktree-agent-a2013627e5ea4ab13'1889 .filter(|_| !fallback)
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1890 .and_then(|_| {
1891 let encoding = request.headers().get("content-encoding").ok().flatten();
1892 pack_cache::cacheable(git, get, protocol, encoding.as_deref(), body.as_deref())
1893 })
1894 .zip(refs_cache::usable(registry::refs_state(&repo.id), now_ms()))
1895 .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 200ms1896 // A kept answer and the free workspace limits, with a kept
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer1897 // credential and, for a push, what the repository holds looked up
1898 // alongside. A kept answer goes back without waiting for the
1899 // credential, which it does not need.
1900 let pushing = write && !get;
1901 let ((answer, pack, (limited, held)), kept_access) = {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1902 let shared = self.shared.as_deref();
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1903 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 200ms1904 async {
1905 match &kept_key {
1906 Some(kept_key) => refs_cache::get(shared, kept_key).await,
1907 None => None,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1908 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1909 },
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1910 async {
1911 match (&pack_key, self.packs.as_deref()) {
1912 (Some(pack_key), Some(packs)) => pack_cache::get(packs, pack_key).await,
1913 _ => None,
1914 }
1915 },
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer1916 futures_util::future::join(self.git_limits(call, git, &repo, env), async {
1917 if pushing { Some(self.held(&repo).await) } else { None }
1918 }),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1919 ));
1920 let kept_access = std::pin::pin!(self.store.kept_access(&key, scope));
1921 match futures_util::future::select(answer_and_limits, kept_access).await {
1922 futures_util::future::Either::Left((first, kept_access)) => {
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer1923 let answered = first.0.is_some() || first.1.is_some() || matches!(first.2.0, Ok(Some(_)) | Err(_));
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1924 (first, if answered { None } else { kept_access.await })
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1925 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1926 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 look1927 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1928 };
1929 timing.mark("kept");
A clone answered from the pack cache is served before the free operation cap: it is not an operation1930 // A kept pack first: it never reaches the store, so it is never an
1931 // operation, and a free workspace past its operation cap still gets
1932 // it. (The other limits are a push's, and a pack is only a fetch.)
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1933 if let Some(kept) = pack {
1934 timing.note("pack", "hit");
1935 let sent = body.as_ref().map_or(0, |body| body.len() as u64);
1936 meters::record(pack_cache::HIT, &key, sent, kept.size);
1937 after.ended(200, None);
1938 after.spawn(env, ctx);
1939 return kept.response();
1940 }
A clone answered from the pack cache is served before the free operation cap: it is not an operation1941 if let Some((response, status, message)) = limited? {
1942 after.ended(status, Some(message.to_owned()));
1943 after.spawn(env, ctx);
1944 return Ok(response);
1945 }
Merge branch 'worktree-agent-a1b995daa94e4e1b7'1946 if pack_key.is_some() {
1947 timing.note("pack", "miss");
1948 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1949 if let (Some((entry, found)), Some(kept_key)) = (answer, &kept_key) {
1950 timing.note("refs", found.as_str());
1951 if found == refs_cache::Found::Shared {
1952 let (kept_key, entry) = (kept_key.clone(), entry.clone());
1953 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 look1954 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1955 // Never reached the store: never an operation.
1956 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 200ms1957 after.ended(200, None);
1958 after.spawn(env, ctx);
1959 return entry.response();
1960 }
1961 if kept_key.is_some() {
1962 timing.note("refs", "miss");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1963 }
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails1964 // An anonymous fetch the store is about to answer: an operation for
1965 // the repository's workspace, so limited per repository (limits.rs).
1966 if limits::counts_as_anonymous_fetch(call, viewer.is_none())
1967 && g1t_kit::limits::check(env, limits::ANONYMOUS_FETCH, repo.id.clone()).await.limited()
1968 {
1969 let response = limits::too_many_anonymous_fetches(&format!("{}/{}", repo.namespace, repo.name))?;
1970 after.ended(429, Some("Too many anonymous fetches.".to_owned()));
1971 after.spawn(env, ctx);
1972 return Ok(response);
1973 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1974 // The store's credential: one made a moment ago, here or in another
1975 // isolate (see store.rs), or a new one.
1976 let access = match kept_access {
1977 Some((access, from)) => {
1978 timing.note("cred", from.as_str());
1979 access
1980 }
1981 None => {
1982 let access = self.store.mint_access(&key, scope).await?;
1983 timing.mark("mint");
1984 timing.note("cred", "mint");
1985 access
1986 }
1987 };
1988 // Should the store turn a kept credential down, a fetch's first
1989 // request is tried again with a new one; the requests after it then
1990 // have that one too.
1991 let again = if get { Some(request.clone()?) } else { None };
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer1992 // A push's checks: workflow files, the rules of the branches and
1993 // tags it changes, saying which rule and why (rules.rs), and push
1994 // protection, which refuses a push that adds a secret
1995 // (secret_scan.rs). See push_checks.rs.
1996 let checks = async |head: &[u8], whole: bool, scan: bool| self.check_receive(&repo, viewer.as_ref(), head, whole, scan).await;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1997 // What a push may bring (pack_limits.rs): the repository's size is
1998 // its own and its pull requests' working copies'.
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer1999 let limits = if let Some(held) = held {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2000 git_http::PushLimits {
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer2001 held,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2002 repo_limit: self.repo_limit,
2003 large: self.large_pushes,
2004 ..git_http::PushLimits::default()
2005 }
2006 } else {
2007 git_http::PushLimits::default()
2008 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2009 let mut outcome = git_http::forward(
2010 request,
2011 body,
2012 git,
2013 &access,
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer2014 checks,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2015 default_branch.as_deref(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2016 limits,
A push says where its time went: receiving it, the rules, the secret scan and the git store's answer each have their own Server-Timing step2017 timing,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2018 )
2019 .await?;
2020 let turned_down = matches!(
2021 &outcome,
2022 git_http::Push::Forwarded(forwarded) if matches!(forwarded.response.status_code(), 401 | 403)
2023 );
2024 if turned_down {
2025 self.store.forget_access(&key).await;
2026 if let Some(again) = again {
2027 let access = self.store.mint_access(&key, scope).await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2028 outcome = git_http::forward(
2029 again,
2030 None,
2031 git,
2032 &access,
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer2033 async |_: &[u8], _: bool, _: bool| Ok(push_checks::Checks::clear()),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2034 default_branch.as_deref(),
2035 git_http::PushLimits::default(),
A push says where its time went: receiving it, the rules, the secret scan and the git store's answer each have their own Server-Timing step2036 timing,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2037 )
2038 .await?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2039 }
2040 }
Prices are what g1t pays plus 20%, from the first second2041 let forwarded =
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2042 match outcome {
Prices are what g1t pays plus 20%, from the first second2043 git_http::Push::Forwarded(forwarded) => forwarded,
2044 git_http::Push::Refused(response) => {
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2045 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 200ms2046 after.spawn(env, ctx);
Prices are what g1t pays plus 20%, from the first second2047 return Ok(response);
2048 }
2049 git_http::Push::Blocked(response) => {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2050 after.ended(403, Some("The push adds a secret.".to_owned()));
2051 after.spawn(env, ctx);
Prices are what g1t pays plus 20%, from the first second2052 return Ok(response);
2053 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2054 git_http::Push::Declined(response, reason) => {
2055 after.ended(403, Some(format!("The push was declined: {reason}.")));
2056 after.spawn(env, ctx);
2057 return Ok(response);
2058 }
Prices are what g1t pays plus 20%, from the first second2059 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2060 if forwarded.from_store {
2061 let received = forwarded
2062 .response
2063 .headers()
2064 .get("content-length")?
2065 .and_then(|length| length.parse().ok())
2066 .unwrap_or(0);
2067 meters::record(call.meter(), &key, forwarded.sent, received);
2068 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2069 timing.mark("store");
2070 let mut response = forwarded.response;
2071 let status = response.status_code();
2072 if write && !get {
2073 // A push: the store has moved its refs once it has answered in
2074 // full, so the answer is read before the change is recorded, and
2075 // only then goes back. Whoever fetches after it sees the push.
2076 let headers = response.headers().clone();
2077 headers.delete("content-length")?;
2078 let report = response.bytes().await?;
2079 self.refs_moved(&repo.id).await;
2080 timing.mark("refs");
2081 response = Response::from_bytes(report)?.with_headers(headers).with_status(status);
2082 } else if let (Some(kept_key), 200) = (&kept_key, status) {
2083 // A miss: this answer is kept for the next to ask.
2084 let headers = response.headers().clone();
2085 headers.delete("content-length")?;
2086 let body = response.bytes().await?;
2087 if let Some(content_type) = headers.get("content-type")? {
2088 let entry = refs_cache::Entry { content_type, body: body.clone() };
2089 if entry.keepable() {
2090 let shared = self.shared.clone();
2091 let kept_key = kept_key.clone();
2092 ctx.wait_until(async move { refs_cache::keep(shared.as_deref(), &kept_key, &entry).await });
2093 }
2094 }
2095 response = Response::from_bytes(body)?.with_headers(headers).with_status(status);
Merge branch 'worktree-agent-a1b995daa94e4e1b7'2096 } else if let (Some(pack_key), Some(packs), true) = (&pack_key, &self.packs, forwarded.from_store) {
2097 // A fresh clone the bucket did not have: counted, and its pack
2098 // kept as it streams to git, when it is a whole one.
2099 meters::record(pack_cache::MISS, &key, forwarded.sent, 0);
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails2100 // Past the repository's limit on writes to the bucket, the pack
2101 // goes to git without being kept (limits.rs).
2102 if status == 200 && !g1t_kit::limits::check(env, limits::PACK_FILL, repo.id.clone()).await.limited() {
Merge branch 'worktree-agent-a1b995daa94e4e1b7'2103 let store_key = key.clone();
2104 let measured = Box::new(move |bytes: u64| meters::record_bytes(pack_cache::MISS, &store_key, 0, bytes));
2105 let (teed, filling) = pack_cache::tee(response, packs.clone(), pack_key, measured)?;
2106 response = teed;
2107 if let Some(filling) = filling {
2108 let pack_key = pack_key.clone();
2109 ctx.wait_until(async move {
2110 let filled = filling.await;
2111 if !matches!(filled, pack_cache::Filled::Kept { .. } | pack_cache::Filled::Abandoned) {
2112 worker::console_warn!("pack {} not kept: {filled:?}", pack_key.as_str());
2113 }
2114 });
2115 }
2116 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2117 }
2118 after.ended(status, None);
Merge branch 'worktree-agent-a8b78c62188153aa3' into claude/cool-newton-7nm8r92119 if status == 200 && (forwarded.pack_bytes > 0 || !forwarded.pushed.is_empty() || !forwarded.deleted.is_empty()) {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2120 after.push = Some(PushDone {
2121 repo,
2122 pushed: forwarded.pushed,
Merge branch 'worktree-agent-a8b78c62188153aa3' into claude/cool-newton-7nm8r92123 deleted: forwarded.deleted,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2124 pack_bytes: forwarded.pack_bytes,
Merge branch 'worktree-agent-a3abfcce648e87dca'2125 caused_by_job: viewer.as_ref().and_then(g1t_contracts::events::job_run_of).map(str::to_owned),
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar2126 // The calendar credits people, never a job's, an agent's or a workspace's token.
2127 credit: viewer
2128 .as_ref()
2129 .filter(|user| user.kind == PrincipalKind::User && g1t_contracts::events::job_run_of(user).is_none())
2130 .map(|user| user.id.clone()),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2131 actor: viewer.map(|user: User| user.id),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2132 unscanned: forwarded.unscanned,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2133 });
2134 }
2135 after.spawn(env, ctx);
2136 Ok(response)
2137 }
2138
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2139 /// The answer for a request a free workspace's limits stop, or a push
2140 /// to a full repository, with its status and reason for the audit log;
2141 /// `None` to go on.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2142 ///
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2143 /// A clone, fetch or push is a git operation, which the git store
2144 /// charges g1t for: counted for billing once the answer has gone back
2145 /// (meters.rs), and a free workspace far past its share is slowed down
2146 /// rather than charged (see git_ops.rs). Whether it is past it is
2147 /// decided from counts this isolate already holds: the database is not
2148 /// asked on the way. A free workspace is never charged for private
2149 /// storage: once its private repositories hold the free amount, pushes
2150 /// to them stop, checked when a push begins so that git shows the
2151 /// 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 200ms2152 async fn git_limits(
2153 &self,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2154 call: git_ops::GitCall,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2155 git: &git_http::GitRequest,
2156 repo: &Repo,
2157 env: &Env,
2158 ) -> Result<Option<(Response, u16, &'static str)>> {
2159 let namespace = git.path.namespace.to_lowercase();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2160 if meters::mapping_now().billable(call.meter()) > 0.0 {
2161 let now = now_ms();
2162 let hour = git_ops::hour_key(&rfc3339(now));
2163 let limits = git_ops::Limits::from_env(env);
2164 if let Some((month, hour_ops)) = git_ops::standing(&namespace, &hour, now)
2165 && git_ops::slow_down(month + 1, hour_ops + 1, limits.free_cap, limits.hourly)
2166 && git_ops::is_free_kept(env.service("BILLING").ok().as_ref(), &namespace).await
2167 {
2168 return Ok(Some((
2169 git_ops::too_many(&namespace, limits.free_cap, limits.hourly)?,
2170 429,
2171 "Too many git operations this hour.",
2172 )));
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2173 }
2174 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2175 if git.service == GitService::ReceivePack && git.endpoint == "info/refs" {
2176 let held = self.held(repo).await;
2177 if held >= self.repo_limit {
2178 let message = format!(
2179 "{}/{} 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",
2180 repo.namespace,
2181 repo.name,
2182 pack_limits::megabytes(held)
2183 );
2184 return Ok(Some((Response::error(message, 403)?, 403, "The repository is full.")));
2185 }
2186 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2187 if git.service == GitService::ReceivePack && git.endpoint == "info/refs" && repo.is_private {
2188 let free = git_ops::free_private_bytes(env);
2189 let held = self.registry.private_bytes(&namespace).await.unwrap_or(0);
2190 if git_ops::storage_full(held, free)
2191 && git_ops::is_free(env.service("BILLING").ok().as_ref(), &namespace).await
2192 {
2193 return Ok(Some((
2194 git_ops::storage_full_response(&namespace, held, free)?,
2195 403,
2196 "Free private storage is full.",
2197 )));
2198 }
2199 }
2200 Ok(None)
2201 }
Prices are what g1t pays plus 20%, from the first second2202
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2203 /// What a repository and its pull requests' working copies hold, as
2204 /// g1t counts it: read for a push's first request, kept a minute for
2205 /// the rest of it.
2206 async fn held(&self, repo: &Repo) -> u64 {
2207 let root = repo.fork_of.clone().unwrap_or_else(|| repo.id.clone());
2208 let now = now_ms();
2209 if let Some(held) = HELD.with(|held| held.borrow().get(&root, now)) {
2210 return held;
2211 }
2212 let held = self.registry.stored_bytes(&root).await.unwrap_or(0).max(0) as u64;
2213 HELD.with(|kept| kept.borrow_mut().put(root, held, now));
2214 held
2215 }
2216
Merge branch 'worktree-agent-a8b78c62188153aa3' into claude/cool-newton-7nm8r92217 /// A `git.deleted` event for each branch or tag a push deleted that is
2218 /// gone from the store: the store can refuse one ref and accept
2219 /// another. As many as a push announces of tags and branches it moves.
2220 async fn publish_deleted(
2221 &self,
2222 repo: &Repo,
2223 deleted: &[(String, String)],
2224 actor: Option<&str>,
2225 caused_by_job: Option<&str>,
2226 ) -> Result<()> {
2227 let stored = self.store.open(&store_key(repo)).await?;
2228 let is_branch = |git_ref: &str| git_ref.starts_with("refs/heads/");
2229 let tags = deleted.iter().filter(|(git_ref, _)| !is_branch(git_ref)).count();
2230 let announced = deleted
2231 .iter()
2232 .filter(|(git_ref, _)| is_branch(git_ref))
2233 .take(MAX_PUSH_BRANCH_EVENTS)
2234 .chain(deleted.iter().filter(|(git_ref, _)| !is_branch(git_ref) && tags <= MAX_PUSH_TAG_EVENTS));
2235 for (git_ref, before) in announced {
2236 let named = git_ref.strip_prefix("refs/heads/").unwrap_or(git_ref);
2237 let gone = match stored.log(named, 1).await {
2238 Ok(head) => head.is_empty(),
2239 Err(error) => {
2240 worker::console_error!("whether {git_ref} of {} was deleted is not known: {error}", repo.name);
2241 false
2242 }
2243 };
2244 if !gone {
2245 continue;
2246 }
2247 self.publish(NewEvent {
2248 kind: "git.deleted",
2249 source: SOURCE,
2250 repo_id: Some(repo.id.clone()),
2251 actor: actor.map(str::to_owned),
2252 data: GitDeleted {
2253 repo_id: repo.id.clone(),
2254 git_ref: git_ref.clone(),
2255 before: before.clone(),
2256 caused_by_job: caused_by_job.map(str::to_owned),
2257 },
2258 })
2259 .await?;
2260 }
2261 Ok(())
2262 }
2263
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2264 /// What a push changed, recorded once git has its answer.
2265 async fn record_push(&self, push: PushDone) -> Result<()> {
2266 let PushDone {
2267 repo,
2268 pushed,
Merge branch 'worktree-agent-a8b78c62188153aa3' into claude/cool-newton-7nm8r92269 deleted,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2270 pack_bytes,
2271 actor,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2272 unscanned,
Merge branch 'worktree-agent-a3abfcce648e87dca'2273 caused_by_job,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar2274 credit,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2275 } = push;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2276 // What the push stored, for billing's storage meter. A failure only
2277 // leaves the count short.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2278 if pack_bytes > 0
2279 && 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 put2280 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2281 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 put2282 }
Merge branch 'worktree-agent-a8b78c62188153aa3' into claude/cool-newton-7nm8r92283 if !deleted.is_empty() {
2284 self.publish_deleted(&repo, &deleted, actor.as_deref(), caused_by_job.as_deref()).await?;
2285 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2286 if pushed.is_empty() {
2287 return Ok(());
2288 }
Prices are what g1t pays plus 20%, from the first second2289 // Artifacts' own push notifications are per repository, which does
2290 // not fit a repo per pull request, so the front end reports pushes
2291 // 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 200ms2292 let stored = self.store.open(&store_key(&repo)).await?;
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails2293 let announced = announced_refs(&pushed, &repo.default_branch);
2294 if announced.len() < pushed.len() {
2295 worker::console_log!(
2296 "push to {}: {} of {} refs announced",
2297 repo.name,
2298 announced.len(),
2299 pushed.len()
2300 );
2301 }
2302 for pushed in announced {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2303 // The store can refuse one ref and accept another, so each
2304 // branch is checked against where it actually is. A tag the
2305 // store cannot read back is taken as pushed.
2306 let moved = match pushed.branch() {
2307 Some(branch) => stored
2308 .log(branch, 1)
2309 .await?
2310 .first()
2311 .is_some_and(|commit| commit.hash == pushed.after),
2312 None => stored.log(&pushed.git_ref, 1).await.map_or(true, |head| {
2313 head.first().is_none_or(|commit| commit.hash == pushed.after)
2314 }),
2315 };
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.2316 // How many commits a branch push brought (push_commits.rs), for
2317 // the event and the person's contribution calendar. A log the
2318 // store cannot read leaves the event without a count and the
2319 // day short.
2320 let commits = match (moved, pushed.branch()) {
2321 (true, Some(_)) => match stored.log(&pushed.after, push_commits::MOST_PER_PUSH + 1).await {
2322 Ok(log) => Some(push_commits::new_commits(&log, pushed.before.as_deref())),
2323 Err(error) => {
2324 worker::console_error!("commits pushed to {} not counted: {error}", repo.name);
2325 None
2326 }
2327 },
2328 _ => None,
2329 };
2330 if let (Some(commits), Some(user_id), Some(branch)) = (commits, credit.as_deref(), pushed.branch())
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar2331 && push_commits::counts(branch, &repo.default_branch)
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.2332 && let Err(error) =
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar2333 push_commits::record(&self.registry.db, user_id, &repo.id, &rfc3339(now_ms())[..10], commits).await
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.2334 {
2335 worker::console_error!("commits pushed to {} not credited: {error}", repo.name);
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar2336 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2337 if moved {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2338 self.publish_git_push(
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2339 &repo,
2340 &pushed.git_ref,
2341 pushed.before.as_deref(),
2342 &pushed.after,
2343 actor.clone(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2344 unscanned,
Merge branch 'worktree-agent-a3abfcce648e87dca'2345 caused_by_job.clone(),
Merge branch 'mirroring' into artifacts-mode2346 false,
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.2347 commits,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2348 )
2349 .await?;
2350 }
2351 }
2352 Ok(())
2353 }
2354}
2355
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails2356/// The most tags one push announces: a push of more (`git push --tags`
2357/// into a new repository, say) announces none of them, so it starts no
2358/// workflows, mirror syncs or package reads, one per tag.
2359const MAX_PUSH_TAG_EVENTS: usize = 3;
2360/// The most branches one push announces. A push of more announces only
2361/// the default branch, if it moved, which the rest of g1t reads from.
2362const MAX_PUSH_BRANCH_EVENTS: usize = 1000;
2363
2364/// The refs of a push that are announced with a `git.push` event each.
2365/// Every ref is stored whatever this says; only the events are capped.
2366fn announced_refs<'a>(pushed: &'a [git_http::Pushed], default_branch: &str) -> Vec<&'a git_http::Pushed> {
2367 let (branches, tags): (Vec<&git_http::Pushed>, Vec<&git_http::Pushed>) =
2368 pushed.iter().partition(|pushed| pushed.branch().is_some());
2369 let mut announced = if branches.len() > MAX_PUSH_BRANCH_EVENTS {
2370 branches.into_iter().filter(|pushed| pushed.branch() == Some(default_branch)).collect()
2371 } else {
2372 branches
2373 };
2374 if tags.len() <= MAX_PUSH_TAG_EVENTS {
2375 announced.extend(tags);
2376 }
2377 announced
2378}
2379
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2380/// A push the store accepted, to be recorded once git has its answer.
2381struct PushDone {
2382 repo: Repo,
2383 pushed: Vec<git_http::Pushed>,
Merge branch 'worktree-agent-a8b78c62188153aa3' into claude/cool-newton-7nm8r92384 /// The branches and tags it asked to delete, each with the commit it
2385 /// pointed to.
2386 deleted: Vec<(String, String)>,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2387 pack_bytes: u64,
2388 actor: Option<String>,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2389 /// Too large to scan for secrets before it was stored.
2390 unscanned: bool,
Merge branch 'worktree-agent-a3abfcce648e87dca'2391 /// The run whose job's token pushed, if one did: its push starts no
2392 /// workflows.
2393 caused_by_job: Option<String>,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar2394 /// The person credited with the push's commits on their contribution
2395 /// calendar: whoever pushed, when that is a person (push_commits.rs).
2396 credit: Option<String>,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2397}
2398
2399/// What a git request leaves for after its answer: its audit entry, with
2400/// how the request ended, and what a push changed.
2401struct AfterGit {
2402 audit: Option<Box<g1t_contracts::audit::NewAuditEntry>>,
2403 status: u16,
2404 message: Option<String>,
2405 push: Option<PushDone>,
2406}
2407
2408impl AfterGit {
2409 fn ended(&mut self, status: u16, message: Option<String>) {
2410 self.status = status;
2411 self.message = message;
2412 }
2413
2414 /// Does the work once the response is on its way. A failure is logged:
2415 /// git has already been told how its request went.
2416 fn spawn(self, env: &Env, ctx: &Context) {
2417 if self.audit.is_none() && self.push.is_none() {
2418 return;
2419 }
2420 let env = env.clone();
2421 ctx.wait_until(async move {
2422 let repos = match service(&env) {
2423 Ok(repos) => repos,
2424 Err(error) => {
2425 worker::console_error!("git request not recorded: {error}");
2426 return;
Prices are what g1t pays plus 20%, from the first second2427 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2428 };
2429 repos.finish_git(self.audit, self.status, self.message).await;
2430 if let Some(push) = self.push
2431 && let Err(error) = repos.record_push(push).await
2432 {
2433 worker::console_error!("push not recorded: {error}");
Prices are what g1t pays plus 20%, from the first second2434 }
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer2435 repos.deferred.settle().await;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2436 });
Prices are what g1t pays plus 20%, from the first second2437 }
2438}
2439
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2440fn service(env: &Env) -> Result<Repos<ArtifactsStore>> {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2441 let shared = shared::Shared::from_env(env).map(Rc::new);
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer2442 let deferred = Rc::new(store::Deferred::default());
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2443 Ok(Repos {
Prices are what g1t pays plus 20%, from the first second2444 registry: Registry { db: env.d1("DB")? },
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer2445 store: ArtifactsStore::new(env, shared.clone(), deferred.clone())?,
2446 deferred,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2447 shared,
Merge branch 'worktree-agent-aaf03bdceac799c89'2448 packs: pack_cache::Packs::from_env(env).map(Rc::new),
Prices are what g1t pays plus 20%, from the first second2449 events: env.service("EVENTS")?,
2450 security: env.service("SECURITY").ok(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2451 billing: env.service("BILLING").ok(),
2452 identity: env.service("IDENTITY").ok(),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2453 work: env.service("WORK").ok(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2454 free_private_bytes: git_ops::free_private_bytes(env),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2455 fork_days: forks::retention_days(env),
2456 repo_limit: env
2457 .var("REPO_STORAGE_LIMIT_BYTES")
2458 .ok()
2459 .and_then(|value| value.to_string().parse().ok())
2460 .unwrap_or(pack_limits::DEFAULT_REPO_LIMIT_BYTES),
2461 large_pushes: git_http::LargePushes::from_var(env.var("LARGE_PUSHES").ok().map(|value| value.to_string()).as_deref()),
2462 placement: shards::Placement::from_vars(
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.2463 env.var("GITSTORE_NEW_REPOS").ok().map(|value| value.to_string()).as_deref(),
2464 env.var("GITSTORE_EU_NAMESPACE").ok().map(|value| value.to_string()).as_deref(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2465 ),
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.2466 limits: shards::limits(env.var("GITSTORE_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 look2467 })
2468}
2469
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2470/// Writes what this isolate metered once the answer has gone back, every
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2471/// few seconds at most: now, or once it is due, waiting in this request's
2472/// `wait_until` so nothing counted is left for a request that may never
2473/// come (meters.rs).
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2474fn flush_later(env: &Env, ctx: &Context) {
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2475 let Some(wait) = meters::plan_flush() else {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2476 return;
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2477 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2478 if let Ok(db) = env.d1("DB") {
Merge branch 'worktree-agent-a57ff9fecefa1eaf7'2479 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 daily2480 }
2481}
2482
Merge branch 'worktree-agent-ac5b181a013e54348'2483/// `/backups/<job id>/parts/<number>`: the job and the part's number.
2484fn backup_part_path(path: &str) -> Option<(String, u16)> {
2485 let rest = path.strip_prefix("/backups/")?;
2486 let (job, number) = rest.split_once("/parts/")?;
2487 let number = number.parse::<u16>().ok()?;
2488 (!job.is_empty() && !job.contains('/')).then(|| (job.to_owned(), number))
2489}
2490
2491fn backups_off<T>() -> Outcome<T> {
2492 Outcome::fail(FailureCode::Conflict, "Backups are off on this installation: it has no storage for them.")
2493}
2494
2495/// One part of a backup's bundle, with the job's token in its header.
2496async fn backup_part(request: &mut Request, env: &Env, repos: &Repos<ArtifactsStore>, job_id: String, number: u16) -> Result<Response> {
2497 let Some(blobs) = backups::storage(env) else {
2498 return reply(&backups_off::<()>());
2499 };
2500 let token = request.headers().get(g1t_contracts::backups::TOKEN_HEADER)?.unwrap_or_default();
2501 let bytes = request.bytes().await?;
2502 let job = g1t_contracts::backups::BackupJobArgs { job_id, token };
2503 reply(&backups::part(&repos.registry.db, &blobs, &job, number, bytes).await?)
2504}
2505
2506#[cfg(test)]
2507mod backup_path_tests {
2508 use super::backup_part_path;
2509
2510 #[test]
2511 fn a_part_is_named_by_its_job_and_number() {
2512 assert_eq!(backup_part_path("/backups/bkp_1/parts/3"), Some(("bkp_1".to_owned(), 3)));
2513 assert_eq!(backup_part_path("/backups/bkp_1/parts/x"), None);
2514 assert_eq!(backup_part_path("/backups//parts/1"), None);
2515 assert_eq!(backup_part_path("/acme/rocket.git/info/refs"), None);
2516 }
2517}
2518
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2519/// Read methods whose answer is an `Outcome`: when the git store is busy,
2520/// the site is told so in words instead of failing the page.
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 252521const OUTCOME_READS: [&str; 7] = ["tree", "blob", "log", "branches", "blame", "compare", "branch_drift"];
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2522
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2523#[event(fetch)]
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms2524async fn fetch(mut request: Request, env: Env, ctx: Context) -> Result<Response> {
Fast pages, required checks on the branch, self-hosted runners, honest incidents2525 let mut repos = service(&env)?;
Merge branch 'worktree-agent-ac5b181a013e54348'2526 // A part of a backup's bundle, as the API passes it on from the
2527 // sandbox: bytes, not JSON (backups.rs).
2528 if request.method() == Method::Put
2529 && let Some((job_id, number)) = backup_part_path(&request.path())
2530 {
2531 let answered = backup_part(&mut request, &env, &repos, job_id, number).await;
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer2532 repos.deferred.hand_over(&ctx);
Merge branch 'worktree-agent-ac5b181a013e54348'2533 flush_later(&env, &ctx);
2534 return answered;
2535 }
Prices are what g1t pays plus 20%, from the first second2536 let Some(method) = rpc_method(&request) else {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2537 let answered = repos.git_http(request, &env, &ctx).await;
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer2538 repos.deferred.hand_over(&ctx);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2539 flush_later(&env, &ctx);
2540 return answered;
Prices are what g1t pays plus 20%, from the first second2541 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents2542 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
2543 // Git over HTTPS above always reads the primary.
2544 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
2545 repos.registry.db = db;
Prices are what g1t pays plus 20%, from the first second2546 let body: serde_json::Value = request.json().await?;
2547
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2548 let answered = async { match method.as_str() {
Prices are what g1t pays plus 20%, from the first second2549 "get" => reply(&repos.get(args(body)?).await?),
2550 "get_by_id" => reply(&repos.get_by_id(args(body)?).await?),
2551 "readable" => {
2552 let a: ReadableArgs = args(body)?;
2553 reply(&repos.registry.readable(&a.ids, &a.viewer).await?)
2554 }
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar2555 // Work's contribution calendar: the commits a person pushed each
2556 // day, in repositories the viewer may read (push_commits.rs).
2557 "commit_days" => {
2558 let a: g1t_contracts::repos::CommitDaysArgs = args(body)?;
2559 reply(&push_commits::days(&repos.registry, &a.user_id, &a.since, &a.viewer).await?)
2560 }
Prices are what g1t pays plus 20%, from the first second2561 "public_namespaces" => {
2562 let a: PublicNamespacesArgs = args(body)?;
2563 reply(&repos.registry.public_namespaces(&a.owner_id).await?)
2564 }
2565 "path_by_id" => {
2566 let a: PathByIdArgs = args(body)?;
2567 reply(
2568 &repos
2569 .registry
2570 .by_id(&a.id)
2571 .await?
2572 .filter(|repo| repo.fork_of.is_none())
2573 .map(|repo| RepoPath {
2574 namespace: repo.namespace,
2575 name: repo.name,
2576 }),
2577 )
2578 }
2579 "list" => {
2580 let a: ListArgs = args(body)?;
2581 reply(
2582 &repos
2583 .registry
2584 .list(
2585 &a.viewer,
2586 a.query.as_deref(),
2587 a.namespace.as_deref(),
2588 a.member_only,
2589 )
2590 .await?,
2591 )
2592 }
2593 "create" => reply(&repos.create(args(body)?).await?),
Merge branch 'mirroring' into artifacts-mode2594 // Services only: a mirror catching up, or pushing out; refs moved
2595 // either way when a takeover is handed back; and the mirror state
2596 // integrations decided (mirror.rs).
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2597 "mirror" => reply(&repos.mirror(args(body)?).await?),
Merge branch 'mirroring' into artifacts-mode2598 "mirror_refs" => reply(&repos.mirror_refs(args(body)?).await?),
2599 "mirror_apply" => reply(&repos.mirror_apply(args(body)?).await?),
2600 "set_mirror" => reply(&repos.set_mirror(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2601 "transfer" => reply(&repos.transfer(args(body)?).await?),
2602 // A repository's lifecycle: see lifecycle.rs.
2603 "delete" => reply(&repos.delete(args(body)?).await?),
2604 "deleted" => reply(&repos.deleted(args(body)?).await?),
2605 "restore" => reply(&repos.restore(args(body)?).await?),
2606 "purge" => reply(&repos.purge(args(body)?).await?),
2607 "purge_due" => reply(&repos.purge_due(args(body)?).await?),
2608 "rename" => reply(&repos.rename(args(body)?).await?),
2609 "archive" => reply(&repos.archive(args(body)?).await?),
2610 "set_visibility" => reply(&repos.set_visibility(args(body)?).await?),
2611 "set_default_branch" => reply(&repos.set_default_branch(args(body)?).await?),
2612 "rename_branch" => reply(&repos.rename_branch(args(body)?).await?),
2613 "resolve_branch" => reply(&repos.resolve_branch(args(body)?).await?),
2614 "status_by_id" => reply(&repos.status_by_id(args(body)?).await?),
2615 "resolve_path" => {
2616 let a: ResolvePathArgs = args(body)?;
2617 reply(&repos.registry.resolve_moved(&a.path).await?)
2618 }
2619 "namespace_count" => {
2620 let a: NamespaceCountArgs = args(body)?;
2621 reply(&repos.registry.count_in(&a.namespace).await?)
2622 }
Prices are what g1t pays plus 20%, from the first second2623 "update" => reply(&repos.update(args(body)?).await?),
2624 "tree" => reply(&repos.tree(args(body)?).await?),
2625 "blob" => reply(&repos.blob(args(body)?).await?),
2626 "log" => reply(&repos.log(args(body)?).await?),
2627 "blame" => reply(&repos.blame(args(body)?).await?),
2628 "fork_for_pull" => reply(&repos.fork_for_pull(args(body)?).await?),
2629 "git_access" => reply(&repos.git_access(args(body)?).await?),
2630 "branches" => reply(&repos.branches(args(body)?).await?),
Branches and Tags pages, each file's last commit, and the branch menu on files2631 "last_commits" => reply(&repos.last_commits(args(body)?).await?),
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 252632 "branch_drift" => reply(&repos.branch_drift(args(body)?).await?),
Branches and Tags pages, each file's last commit, and the branch menu on files2633 "tags" => reply(&repos.tags(args(body)?).await?),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb972634 // The About: what the Files page shows beside the files (about.rs).
2635 // What is kept behind the head is worked out again after the answer.
2636 "about" => {
2637 let (answer, refresh) = repos.about(args(body)?).await?;
2638 about::refresh_later(&env, &ctx, refresh);
2639 reply(&answer)
2640 }
2641 "languages" => {
2642 let (answer, refresh) = repos.languages(args(body)?).await?;
2643 about::refresh_later(&env, &ctx, refresh);
2644 reply(&answer)
2645 }
2646 "contributors" => {
2647 let (answer, refresh) = repos.contributors(args(body)?).await?;
2648 about::refresh_later(&env, &ctx, refresh);
2649 reply(&answer)
2650 }
2651 "license" => {
2652 let (answer, refresh) = repos.license(args(body)?).await?;
2653 about::refresh_later(&env, &ctx, refresh);
2654 reply(&answer)
2655 }
2656 "stars" => reply(&repos.stars(args(body)?).await?),
2657 "star" => reply(&repos.star(args(body)?).await?),
2658 "stargazers" => reply(&repos.stargazers(args(body)?).await?),
2659 "starred" => reply(&repos.starred(args(body)?).await?),
2660 "releases" => reply(&repos.releases(args(body)?).await?),
2661 "release" => reply(&repos.release(args(body)?).await?),
2662 "create_release" => reply(&repos.create_release(args(body)?).await?),
2663 "update_release" => reply(&repos.update_release(args(body)?).await?),
2664 "delete_release" => reply(&repos.delete_release(args(body)?).await?),
Prices are what g1t pays plus 20%, from the first second2665 "head" => reply(&repos.head(args(body)?).await?),
2666 "behind" => reply(&repos.behind(args(body)?).await?),
2667 "divergence" => reply(&repos.divergence(args(body)?).await?),
2668 "land" => reply(&repos.land(args(body)?).await?),
2669 "update_pull_branch" => reply(&repos.update_pull_branch(args(body)?).await?),
A pull request's `pull_request` runs build the pull request merged into its base branch, as on GitHub. g1t keeps `refs/pull/<number>/head` at every open pull request's head (a fork's or working copy's head commit is copied into the repository) and `refs/pull/<number>/merge` at a test merge commit of that head into the base branch's latest commit, moved whenever the head is pushed to, the pull request is opened or reopened, or its base changes; both can be fetched from the repository. A run's `github.sha` and `GITHUB_SHA` are that merge commit, `github.event.pull_request.merge_commit_sha` names it, `actions/checkout` fetches it as `+<sha>:refs/remotes/pull/<number>/merge` from the repository itself, and the workflow files and local reusable workflows are read from it; the run, its statuses and its check runs stay on the head, so required checks hold merges as before, and `pull_request_target` is unchanged. A pull request with conflicts starts no `pull_request` workflows, as on GitHub, and its checks box says why. g1t makes the merge without a sandbox, merging a file both sides changed line by line as git does; what it cannot merge itself (a rename on one side against a change on the other, more than 300 changed files) builds the head as before. The Actions and Checks guides say so.2670 // Services only: a pull request's test merge commit (test_merge.rs).
2671 "pull_merge_ref" => reply(&repos.pull_merge_ref(args(body)?).await?),
Prices are what g1t pays plus 20%, from the first second2672 "delete_branch" => reply(&repos.delete_branch(args(body)?).await?),
Fast pages, required checks on the branch, self-hosted runners, honest incidents2673 "commit_file" => reply(&repos.commit_file(args(body)?).await?),
Prices are what g1t pays plus 20%, from the first second2674 "compare" => reply(&repos.compare(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2675 // Services only: a pull request's commits, as rules look at them (rules.rs).
2676 "inspect_commits" => reply(&repos.inspect_commits(args(body)?).await?),
Prices are what g1t pays plus 20%, from the first second2677 "scan_history" => reply(&repos.scan_history(args(body)?).await?),
2678 "find_lockfiles" => reply(&repos.find_lockfiles(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2679 "match_pattern" => reply(&repos.match_pattern(args(body)?).await?),
2680 "check_secret" => reply(&repos.check_secret(args(body)?).await?),
Prices are what g1t pays plus 20%, from the first second2681 "list_files" => reply(&repos.list_files(args(body)?).await?),
2682 "changed_files" => reply(&repos.changed_files(args(body)?).await?),
2683 "read_blobs" => reply(&repos.read_blobs(args(body)?).await?),
Composer from the workspace's own repositories, and go get from g1t.sh2684 // Services only: what the Composer registry builds packages from.
2685 "refs" => reply(&repos.refs_of(args(body)?).await?),
2686 "raw_file" => reply(&repos.raw_file(args(body)?).await?),
2687 "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 put2688 "visibility" => {
2689 let a: g1t_contracts::repos::VisibilityArgs = args(body)?;
2690 reply(&repos.registry.visibility(&a.paths).await?)
2691 }
2692 "storage" => reply(&repos.registry.storage().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2693 "git_operations" => {
2694 let a: GitOperationsArgs = args(body)?;
2695 reply(&git_ops::totals(&repos.registry.db, &a.month, a.since.as_deref(), a.namespace.as_deref().map(str::to_lowercase).as_deref()).await?)
2696 }
Merge main (membership, two-factor, GitHub repo roles) into tokens2697 // Identity, once: who created each repository (members.rs there).
2698 "repo_creators" => {
2699 let a: AllIdsArgs = args(body)?;
2700 let limit = a.limit.clamp(1, 500);
2701 let repos = repos.registry.creators_after(a.after.as_deref(), limit).await?;
2702 let next = (repos.len() == limit as usize).then(|| repos.last().map(|repo| repo.id.clone())).flatten();
2703 reply(&CreatorPage { repos, next })
2704 }
Prices are what g1t pays plus 20%, from the first second2705 "all_ids" => {
2706 let a: AllIdsArgs = args(body)?;
2707 let limit = a.limit.clamp(1, 500);
2708 let ids = repos.registry.ids_after(a.after.as_deref(), limit).await?;
2709 let next = (ids.len() == limit as usize).then(|| ids.last().cloned()).flatten();
2710 reply(&IdPage { ids, next })
2711 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2712 // The raw meters of the git store, for reconciling with Cloudflare
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.2713 // (meters.rs, scripts/ops/gitstore-usage.mjs).
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2714 "artifacts_usage" => {
2715 let a: meters::UsageArgs = args(body)?;
2716 reply(&meters::usage(&repos.registry.db, &a).await?)
2717 }
2718 "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)2719 // Billing: the workspace each pull request's working copy is counted
2720 // for, so Cloudflare's own count of `pulls--<id>` shares out too.
2721 "pull_owners" => {
2722 #[derive(serde::Deserialize)]
2723 struct PullOwnersArgs {
2724 pulls: Vec<String>,
2725 }
2726 let a: PullOwnersArgs = args(body)?;
2727 let pulls: Vec<String> = a.pulls.into_iter().take(500).collect();
2728 reply(&serde_json::json!({ "owners": meters::pull_owners(&repos.registry.db, &pulls).await? }))
2729 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2730 // Services only: which meters are operations, changed without a deploy.
2731 "set_operation_mapping" => {
2732 let row: meters::MappingRow = args(body)?;
2733 meters::set_mapping(&repos.registry.db, &row, &rfc3339(now_ms())).await?;
2734 reply(&meters::read_mapping(&repos.registry.db).await?)
2735 }
Merge branch 'worktree-agent-ac5b181a013e54348'2736 // Backups (backups.rs): the runner's sweep claims queued ones, and
2737 // each sandbox, through the API, asks for its job and says how it went.
2738 "claim_backups" => {
2739 let a: g1t_contracts::backups::ClaimBackupsArgs = args(body)?;
2740 let blobs = backups::storage(&env);
2741 reply(&backups::claim(&repos.registry.db, blobs.as_ref(), &a, now_ms()).await?)
2742 }
2743 "backup_spec" => match backups::storage(&env) {
2744 Some(blobs) => {
2745 let a: g1t_contracts::backups::BackupJobArgs = args(body)?;
2746 let every = backups::Settings::from_env(&env).full_every;
2747 reply(&backups::spec(&repos.registry, &blobs, &repos.store, &a, every, now_ms()).await?)
2748 }
2749 None => reply(&backups_off::<bool>()),
2750 },
2751 "backup_complete" => match backups::storage(&env) {
2752 Some(blobs) => reply(&backups::complete(&repos.registry, &blobs, &args(body)?, now_ms()).await?),
2753 None => reply(&backups_off::<bool>()),
2754 },
2755 "backup_fail" => match backups::storage(&env) {
2756 Some(blobs) => reply(&backups::fail(&repos.registry.db, &blobs, &args(body)?).await?),
2757 None => reply(&backups_off::<bool>()),
2758 },
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2759 // How the git store has been answering, for the status page.
2760 "store_health" => {
2761 let a: meters::HealthArgs = args(body)?;
2762 reply(&meters::health(&repos.registry.db, &a).await?)
2763 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'2764 // Where repositories may be kept, for a workspace's settings.
2765 "storage_options" => reply(&repos.storage_options()),
2766 // Services and operators only: how each namespace stands, and
2767 // moving a repository between them (namespaces.rs, moves.rs).
2768 "namespaces" => reply(&repos.standings().await?),
2769 "move_repository" => reply(&repos.move_repository(args(body)?).await?),
2770 "repository_moves" => {
2771 let a: moves::ListMovesArgs = args(body)?;
2772 reply(&repos.registry.moves(a.limit.unwrap_or(50)).await?)
2773 }
Prices are what g1t pays plus 20%, from the first second2774 _ => Response::error("Unknown method", 404),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2775 } }
2776 .await;
2777 // The git store is busy: said in words, with when to try again.
2778 let answered = match answered {
2779 Err(error) => match resilience::busy(&error.to_string()) {
2780 Some(busy) if OUTCOME_READS.contains(&method.as_str()) => {
2781 reply(&Outcome::<()>::fail(FailureCode::Conflict, busy.message().trim()))
2782 }
2783 Some(busy) => {
2784 let response = Response::error(busy.message(), 503)?;
2785 response.headers().set("retry-after", &busy.retry_after.to_string())?;
2786 Ok(response)
2787 }
2788 None => Err(error),
2789 },
2790 answered => answered,
Fast pages, required checks on the branch, self-hosted runners, honest incidents2791 };
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer2792 repos.deferred.hand_over(&ctx);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2793 flush_later(&env, &ctx);
Fast pages, required checks on the branch, self-hosted runners, honest incidents2794 served.finish(answered)
Prices are what g1t pays plus 20%, from the first second2795}
2796
Merge branch 'worktree-agent-ac5b181a013e54348'2797/// The nightly cron in wrangler.jsonc: tonight's backups are queued.
2798const BACKUP_CRON: &str = "53 2 * * *";
2799
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2800/// The hourly sweep: deleted repositories whose time to be restored has
Merge branch 'worktree-agent-ac5b181a013e54348'2801/// passed are purged. See lifecycle.rs. And, at [`BACKUP_CRON`], the
2802/// 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 look2803#[event(scheduled)]
Merge branch 'worktree-agent-ac5b181a013e54348'2804async fn scheduled(event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2805 let repos = match service(&env) {
2806 Ok(repos) => repos,
2807 Err(error) => {
2808 worker::console_error!("repos: the sweep could not start: {error}");
2809 return;
2810 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2811 };
Merge branch 'worktree-agent-ac5b181a013e54348'2812 if event.cron() == BACKUP_CRON {
2813 let Some(blobs) = backups::storage(&env) else { return };
2814 match backups::nightly(&repos.registry.db, &blobs, backups::Settings::from_env(&env), now_ms()).await {
2815 Ok(night) => worker::console_log!("repos: queued {} backups, removed {} of purged repositories", night.queued, night.pruned),
2816 Err(error) => worker::console_error!("repos: backups could not be queued: {error}"),
2817 }
2818 return;
2819 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2820 match repos.purge_due(PurgeDueArgs::default()).await {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2821 Ok(0) => {}
2822 Ok(count) => worker::console_log!("repos: purged {count} deleted repositories"),
2823 Err(error) => worker::console_error!("repos: the purge sweep failed: {error}"),
2824 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2825 // Pull requests' working copies whose time has come (forks.rs).
2826 match repos.retire_due().await {
2827 Ok(0) => {}
2828 Ok(count) => worker::console_log!("repos: removed {count} pull request working copies"),
2829 Err(error) => worker::console_error!("repos: the working copy sweep failed: {error}"),
2830 }
Merge branch 'worktree-agent-a2013627e5ea4ab13'2831 // Repositories moving between namespaces, and old copies (moves.rs).
2832 match repos.run_moves().await {
2833 Ok(0) => {}
2834 Ok(count) => worker::console_log!("repos: moved {count} repositories between namespaces"),
2835 Err(error) => worker::console_error!("repos: the move sweep failed: {error}"),
2836 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2837 meters::flush(&repos.registry.db).await;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2838}
2839
2840/// Events from the bus. A workspace's rename: its repositories move to the
2841/// workspace's current slug, asked of identity by id, so a repeated or late
2842/// 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 member2843/// were. A workspace's deletion: its repositories are deleted with it,
2844/// restored with it, or purged with it.
Prices are what g1t pays plus 20%, from the first second2845#[event(queue)]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2846async fn queue(batch: MessageBatch<Event>, env: Env, ctx: Context) -> Result<()> {
Prices are what g1t pays plus 20%, from the first second2847 let registry = Registry { db: env.d1("DB")? };
2848 let identity = env.service("IDENTITY")?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2849 let handled = handle_events(&batch, &env, &registry, &identity).await;
2850 flush_later(&env, &ctx);
2851 handled
2852}
2853
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails2854/// Each event is acknowledged or retried on its own, so one that fails is
2855/// tried again without the others before and after it running twice.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2856async fn handle_events(batch: &MessageBatch<Event>, env: &Env, registry: &Registry, identity: &Fetcher) -> Result<()> {
Prices are what g1t pays plus 20%, from the first second2857 for message in batch.messages()? {
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails2858 match handle_event(message.body(), env, registry, identity).await {
2859 Ok(()) => message.ack(),
2860 Err(error) => {
2861 worker::console_error!("repos: event {} failed: {error}", message.body().id);
2862 message.retry();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2863 }
2864 }
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails2865 }
2866 Ok(())
2867}
2868
2869async fn handle_event(event: &Event, env: &Env, registry: &Registry, identity: &Fetcher) -> Result<()> {
2870 // A pull request merged, closed or reopened: its working copy is
2871 // kept or let go (forks.rs).
2872 if let Some(change) = forks::pull_change(&event.kind) {
2873 let Some(pull_id) = forks::pull_id_of(&event.data) else {
2874 worker::console_error!("{} {} names no pull request", event.kind, event.id);
2875 return Ok(());
2876 };
2877 let repos = service(env)?;
2878 match change {
2879 forks::PullChange::Settled => repos.pull_settled(&pull_id).await?,
2880 forks::PullChange::Reopened => repos.pull_reopened(&pull_id).await?,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member2881 }
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails2882 return Ok(());
2883 }
2884 // A workspace deleted, restored or purged: its repositories go with
2885 // it, come back with it, or are purged with it (lifecycle.rs).
2886 if event.kind == "workspace.deleting" {
2887 match serde_json::from_value::<WorkspaceDeleting>(event.data.clone()) {
2888 Ok(deleting) => service(env)?.delete_with_workspace(&deleting, &protected_workspaces(env)).await?,
2889 Err(_) => worker::console_error!("workspace.deleting {} could not be read", event.id),
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member2890 }
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails2891 return Ok(());
2892 }
2893 if event.kind == "workspace.restored" {
2894 match serde_json::from_value::<WorkspaceRestored>(event.data.clone()) {
2895 Ok(restored) => service(env)?.restore_with_workspace(&restored).await?,
2896 Err(_) => worker::console_error!("workspace.restored {} could not be read", event.id),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2897 }
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails2898 return Ok(());
2899 }
2900 if event.kind == "workspace.deleted" {
2901 match serde_json::from_value::<WorkspaceDeleted>(event.data.clone()) {
2902 Ok(deleted) => service(env)?.purge_workspace(&deleted, &protected_workspaces(env)).await?,
2903 Err(_) => worker::console_error!("workspace.deleted {} could not be read", event.id),
Merge leftovers: plan activity filtered in the query, pushes counted by account, ghost during the deletion window, profile time zones (identity 0039)2904 }
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails2905 return Ok(());
2906 }
2907 // An account deleted or restored: kept contributors that name it
2908 // (or ghost, for a restore) are worked out again, so it shows as
2909 // ghost for its 30 days, and as itself again if restored.
2910 if let Some(needle) = stats::shown_differently(&event.kind, &event.data) {
2911 let db = env.d1("DB")?;
2912 if let Err(error) = stats::rework_naming(&db, &needle).await {
2913 worker::console_error!("{} {}: contributors not marked to be counted again: {error}", event.kind, event.id);
Prices are what g1t pays plus 20%, from the first second2914 }
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails2915 return Ok(());
2916 }
2917 if event.kind != "workspace.renamed" {
2918 return Ok(());
2919 }
2920 let Ok(renamed) = serde_json::from_value::<WorkspaceRenamed>(event.data.clone()) else {
2921 worker::console_error!("workspace.renamed {} could not be read", event.id);
2922 return Ok(());
2923 };
2924 let names: HashMap<String, String> = g1t_kit::call(
2925 identity,
2926 "usernames",
2927 &g1t_contracts::identity::UsernamesArgs {
2928 ids: vec![renamed.workspace_id.clone()],
2929 },
2930 )
2931 .await?;
2932 let current = names
2933 .get(&renamed.workspace_id)
2934 .cloned()
2935 .unwrap_or_else(|| renamed.to.clone());
2936 let left = registry
2937 .rename_namespace(&renamed.stale_slugs(&current), &current)
Prices are what g1t pays plus 20%, from the first second2938 .await?;
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails2939 if left > 0 {
2940 worker::console_error!(
2941 "{left} repositories stayed under {} or {}: {current} already has repositories of the same names",
2942 renamed.from,
2943 renamed.to
2944 );
Prices are what g1t pays plus 20%, from the first second2945 }
2946 Ok(())
2947}
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2948
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member2949/// The workspaces whose repositories never go with a deletion, whatever is
2950/// published: `PROTECTED_WORKSPACES` if set here, and Flagon's always.
2951fn protected_workspaces(env: &Env) -> Vec<String> {
2952 let configured = env.var("PROTECTED_WORKSPACES").ok().map(|v| v.to_string());
2953 g1t_contracts::identity::protected_names(configured.as_deref())
2954}
2955
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca2956/// What a token's own limits say about git on the repository `repo`
2957/// (`owner/name`), before anyone's role is asked: why it is refused, or
2958/// `None`. `source` is the repository a pull request's working copy at
2959/// `repo` belongs to, which a workflow job's token reaches too. `public`
2960/// is whether anyone may read it, and `exists` whether there is one.
2961///
2962/// A job's token and a deploy key reach their own repository only. Its
2963/// scopes decide the rest: `code:read` to read a private repository,
2964/// `code:write` to push, which a read-only deploy key never has. A deploy
2965/// key never makes a repository by pushing to an empty address.
2966pub(crate) fn git_token_refusal(
2967 access: &g1t_contracts::scopes::TokenAccess,
2968 repo: &str,
2969 source: Option<&str>,
2970 write: bool,
2971 public: bool,
2972 exists: bool,
2973) -> Option<String> {
2974 if let Some(refused) = g1t_contracts::scopes::decide_repo(access, repo)
2975 && !source.is_some_and(|source| access.reaches(source))
2976 {
2977 return Some(refused.reason.unwrap_or_default());
2978 }
2979 let decision = g1t_contracts::scopes::decide_git(access, write, public);
2980 if !decision.allowed {
2981 return Some(decision.reason.unwrap_or_default());
2982 }
2983 if access.deploy_key.is_some() && !exists {
2984 return Some(format!("This deploy key is for {repo}, which is not there any more."));
2985 }
2986 None
2987}
2988
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2989/// The repository a push to a path that does not exist yet creates: private,
2990/// so nothing pushed by mistake is published. An owner makes it public on
2991/// purpose (`POST /repos/{owner}/{repo}/visibility`).
2992fn push_to_create(owner: &User, path: &RepoPath) -> CreateArgs {
2993 CreateArgs {
2994 owner: owner.clone(),
2995 namespace: path.namespace.clone(),
2996 name: path.name.clone(),
2997 description: None,
2998 is_private: true,
2999 import_url: None,
3000 import_token: None,
Merge branch 'mirroring' into artifacts-mode3001 mirror: None,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily3002 }
3003}
3004
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches3005/// Whether `delete_branch` may remove `branch`: one of g1t's own
3006/// (`g1t-…`), or one whose tip the caller names, such as a dependency
3007/// update's branch after its pull request closed.
3008fn deletable_branch(branch: &str, head: Option<&str>) -> bool {
3009 !branch.is_empty() && (branch.starts_with(G1T_BRANCH_PREFIX) || head.is_some_and(|head| !head.is_empty()))
3010}
3011
3012#[cfg(test)]
3013mod delete_branch_tests {
3014 use super::deletable_branch;
3015
3016 #[test]
3017 fn only_g1t_branches_or_a_named_tip_are_deleted() {
3018 assert!(deletable_branch("g1t-queue-12", None));
3019 assert!(!deletable_branch("g1t/security/sharp-0.35.5", None));
3020 assert!(deletable_branch("g1t/security/sharp-0.35.5", Some("abc123")));
3021 assert!(!deletable_branch("feature", Some("")));
3022 assert!(!deletable_branch("", Some("abc123")));
3023 }
3024}
3025
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily3026#[cfg(test)]
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails3027mod announced_refs_tests {
3028 use super::*;
3029
3030 fn pushed(git_ref: String) -> git_http::Pushed {
3031 git_http::Pushed { git_ref, before: None, after: "abc".into() }
3032 }
3033
3034 fn refs(announced: Vec<&git_http::Pushed>) -> Vec<&str> {
3035 announced.into_iter().map(|pushed| pushed.git_ref.as_str()).collect()
3036 }
3037
3038 #[test]
3039 fn a_few_tags_are_announced_and_many_are_not() {
3040 let few: Vec<_> = (1..=3).map(|n| pushed(format!("refs/tags/v{n}"))).chain([pushed("refs/heads/main".into())]).collect();
3041 assert_eq!(refs(announced_refs(&few, "main")), ["refs/heads/main", "refs/tags/v1", "refs/tags/v2", "refs/tags/v3"]);
3042 let many: Vec<_> = (1..=10_000).map(|n| pushed(format!("refs/tags/v{n}"))).chain([pushed("refs/heads/main".into())]).collect();
3043 assert_eq!(refs(announced_refs(&many, "main")), ["refs/heads/main"]);
3044 }
3045
3046 #[test]
3047 fn past_the_branch_cap_only_the_default_branch_is_announced() {
3048 let at_cap: Vec<_> = (0..MAX_PUSH_BRANCH_EVENTS).map(|n| pushed(format!("refs/heads/b{n}"))).collect();
3049 assert_eq!(announced_refs(&at_cap, "main").len(), MAX_PUSH_BRANCH_EVENTS);
3050 let over: Vec<_> = (0..=MAX_PUSH_BRANCH_EVENTS)
3051 .map(|n| pushed(format!("refs/heads/b{n}")))
3052 .chain([pushed("refs/heads/main".into())])
3053 .collect();
3054 assert_eq!(refs(announced_refs(&over, "main")), ["refs/heads/main"]);
3055 assert!(announced_refs(&over, "trunk").is_empty());
3056 }
3057}
3058
3059#[cfg(test)]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily3060mod push_to_create_tests {
3061 use super::*;
3062
3063 #[test]
3064 fn a_pushed_repository_starts_private() {
3065 let owner: User = serde_json::from_value(serde_json::json!({ "id": "usr_1", "username": "ada" })).unwrap();
3066 let args = push_to_create(&owner, &RepoPath { namespace: "acme".into(), name: "site".into() });
3067 assert!(args.is_private);
3068 assert_eq!((args.namespace.as_str(), args.name.as_str()), ("acme", "site"));
3069 }
3070}
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca3071
3072#[cfg(test)]
3073mod deploy_key_git_tests {
3074 use super::*;
3075 use g1t_contracts::deploy_keys;
3076
3077 fn key(read_only: bool) -> User {
3078 deploy_keys::principal("wsp_acme", "acme", deploy_keys::access("dk_1", "CI", "acme/rocket", read_only))
3079 }
3080
3081 fn rocket(private: bool) -> Repo {
3082 serde_json::from_value(serde_json::json!({
3083 "id": "rep_rocket",
3084 "namespace": "acme",
3085 "name": "rocket",
3086 "description": null,
3087 "isPrivate": private,
3088 "ownerId": "usr_owner",
3089 "defaultBranch": "main",
3090 "forkOf": null,
3091 "protected": false,
3092 "createdAt": "",
3093 }))
3094 .unwrap()
3095 }
3096
3097 fn refusal(user: &User, repo: &str, write: bool, exists: bool) -> Option<String> {
3098 git_token_refusal(user.token.as_deref().unwrap(), repo, None, write, false, exists)
3099 }
3100
3101 #[test]
3102 fn a_read_only_deploy_key_clones_its_repository_and_never_pushes() {
3103 let user = key(true);
3104 assert_eq!(refusal(&user, "acme/rocket", false, true), None);
3105 assert!(refusal(&user, "acme/rocket", true, true).unwrap().contains("read-only"));
3106 // Its role is a workspace token's: it reads a private repository.
3107 assert!(registry::can_read(&rocket(true), &Some(user)));
3108 }
3109
3110 #[test]
3111 fn a_deploy_key_with_write_access_pushes_to_its_repository() {
3112 let user = key(false);
3113 assert_eq!(refusal(&user, "acme/rocket", true, true), None);
3114 assert!(registry::can_write(&rocket(true), &Some(user)));
3115 }
3116
3117 #[test]
3118 fn a_deploy_key_reaches_no_other_repository() {
3119 let user = key(false);
3120 for other in ["acme/booster", "other/rocket"] {
3121 for write in [false, true] {
3122 let why = refusal(&user, other, write, true).expect(other);
3123 assert!(why.contains("deploy key is for acme/rocket"), "{why}");
3124 }
3125 }
3126 // Not even a pull request's working copy of another repository.
3127 let token = user.token.as_deref().unwrap();
3128 assert!(git_token_refusal(token, "pulls/pr_1", Some("acme/booster"), false, false, true).is_some());
3129 }
3130
3131 #[test]
3132 fn a_deploy_key_never_creates_a_repository() {
3133 let why = refusal(&key(false), "acme/rocket", true, false).unwrap();
3134 assert!(why.contains("not there"), "{why}");
3135 }
3136}

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