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,172 linesCodeBlameRaw
1//! 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
8mod about;
9mod archive_links;
10mod backups;
11mod blame;
12mod catch_up;
13mod coalesce;
14mod commit_file;
15mod contributors;
16mod copy;
17mod diff;
18mod drift;
19mod fallback;
20mod forks;
21mod git_http;
22mod git_ops;
23mod hidden_refs;
24mod import;
25mod land;
26mod languages;
27mod last_commits;
28mod license;
29mod lifecycle;
30mod limits;
31mod listing;
32mod meters;
33mod moves;
34mod namespaces;
35mod pack_cache;
36mod pack_limits;
37mod push_checks;
38mod push_commits;
39mod refs;
40mod refs_cache;
41mod registry;
42mod resilience;
43mod rule_facts;
44mod rules;
45mod run_access;
46mod secret_scan;
47mod shards;
48mod shared;
49mod signatures;
50mod stats;
51mod store;
52mod test_merge;
53mod transfer;
54mod workflow_gate;
55
56use g1t_contracts::events::{
57 Event, GitDeleted, GitPush, NewEvent, Publish, RepoCreated, RepoForked, RepoUpdated, WorkspaceDeleted,
58 WorkspaceDeleting, WorkspaceRenamed, WorkspaceRestored,
59};
60use g1t_contracts::access::{self, Capability};
61use g1t_contracts::repos::*;
62use g1t_contracts::time::rfc3339;
63use g1t_contracts::{FailureCode, Outcome, PrincipalKind, Role, User, Viewer, is_valid_repo_name, new_id};
64use g1t_kit::{args, now_ms, reply, rpc_method};
65use std::collections::{HashMap, HashSet, VecDeque};
66use std::rc::Rc;
67
68use serde::Serialize;
69use worker::{
70 Context, Env, Fetcher, MessageBatch, MessageExt, Method, Request, Response, Result, ScheduleContext, ScheduledEvent,
71 event,
72};
73
74use registry::{Registry, can_read, can_write, store_key};
75use store::{ArtifactsStore, GitRepo, GitStore, Scope};
76
77/// Namespace that holds every pull request's fork: `pulls/<pull id>`.
78pub(crate) const PULLS_NAMESPACE: &str = "pulls";
79const MAX_TEXT_BYTES: usize = 512 * 1024;
80/// How far back a pull request may have forked and still be landed.
81const MAX_ANCESTRY: u32 = 1000;
82/// The most tags a repository's Tags page reads and lists.
83const MAX_TAGS_READ: usize = 100;
84/// Branch heads measured in one `branch_drift` call.
85const MAX_DRIFT_HEADS: usize = 100;
86/// How long a `last_commits` walk asked without a budget runs before it
87/// stops and keeps its progress for the next call. The site asks from a
88/// waitUntil, which may run 30 s past its response.
89const LAST_COMMITS_WALK_MS: u64 = 20_000;
90
91/// One path segment, percent-encoded for a cache key.
92fn urlencoding_segment(segment: &str) -> String {
93 segment
94 .bytes()
95 .map(|b| if b.is_ascii_alphanumeric() || b"-._~".contains(&b) { (b as char).to_string() } else { format!("%{b:02X}") })
96 .collect()
97}
98const MAX_DESCRIPTION_CHARS: usize = 200;
99pub(crate) const SOURCE: &str = "repos";
100pub(crate) const UNVERIFIED: &str = "Confirm your email address first. Check your inbox, or resend the link from the banner on g1t.sh.";
101
102pub(crate) fn not_found<T>() -> Outcome<T> {
103 Outcome::fail(FailureCode::NotFound, "Repository not found.")
104}
105
106/// Decoded text, or `None` when the file is too large or looks binary.
107/// Whether a ref is a full commit hash rather than a branch name.
108fn is_commit_hash(git_ref: &str) -> bool {
109 git_ref.len() == 40 && git_ref.bytes().all(|b| b.is_ascii_hexdigit())
110}
111
112fn text_of(bytes: Vec<u8>) -> Option<String> {
113 if bytes.len() > MAX_TEXT_BYTES || bytes.contains(&0) {
114 return None;
115 }
116 Some(String::from_utf8_lossy(&bytes).into_owned())
117}
118
119fn is_readme(name: &str) -> bool {
120 matches!(
121 name.to_lowercase().as_str(),
122 "readme" | "readme.md" | "readme.markdown" | "readme.txt"
123 )
124}
125
126/// Whether `ancestor` is reachable from the newest commit in `history`.
127///
128/// `history` is the first-parent chain, which is all the store lists; a fork
129/// that merged the target branch in has the target's head on a second
130/// parent, so the walk follows every parent.
131async fn descends_from<R: GitRepo>(repo: &R, history: &[Commit], ancestor: &str) -> Result<bool> {
132 let known: HashMap<&str, &[String]> = history
133 .iter()
134 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
135 .collect();
136 let mut seen = HashSet::new();
137 let mut queue: Vec<String> = history
138 .first()
139 .map(|c| c.hash.clone())
140 .into_iter()
141 .collect();
142 while let Some(hash) = queue.pop() {
143 if hash == ancestor {
144 return Ok(true);
145 }
146 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
147 continue;
148 }
149 match known.get(hash.as_str()) {
150 Some(parents) => queue.extend(parents.iter().cloned()),
151 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
152 }
153 }
154 Ok(false)
155}
156
157/// The commit closest to the newest in `history` that is also in `shared`:
158/// where a fork and the repository it came from last agreed.
159async fn nearest_ancestor_in<R: GitRepo>(
160 repo: &R,
161 history: &[Commit],
162 shared: &HashSet<String>,
163) -> Result<Option<String>> {
164 let known: HashMap<&str, &[String]> = history
165 .iter()
166 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
167 .collect();
168 let mut seen = HashSet::new();
169 let mut queue: VecDeque<String> = history
170 .first()
171 .map(|c| c.hash.clone())
172 .into_iter()
173 .collect();
174 while let Some(hash) = queue.pop_front() {
175 if shared.contains(&hash) {
176 return Ok(Some(hash));
177 }
178 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
179 continue;
180 }
181 match known.get(hash.as_str()) {
182 Some(parents) => queue.extend(parents.iter().cloned()),
183 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
184 }
185 }
186 Ok(None)
187}
188
189thread_local! {
190 /// Targets' sides of mergeability, by head (coalesce.rs).
191 static TARGETS: std::cell::RefCell<coalesce::Memo<coalesce::TargetKey, Rc<coalesce::TargetSide>>> =
192 std::cell::RefCell::new(coalesce::Memo::new(coalesce::TARGET_TTL_MS, 32));
193 /// What targets changed between two trees.
194 static THEIRS: std::cell::RefCell<coalesce::Memo<coalesce::TheirsKey, (Vec<String>, bool)>> =
195 std::cell::RefCell::new(coalesce::Memo::new(coalesce::THEIRS_TTL_MS, 256));
196 /// What repositories hold, as read for a push's first request, for the
197 /// same push's second: a push's POST does not wait on the database.
198 static HELD: std::cell::RefCell<coalesce::Memo<String, u64>> =
199 std::cell::RefCell::new(coalesce::Memo::new(60_000, 512));
200}
201
202pub(crate) struct Repos<S: GitStore> {
203 registry: Registry,
204 store: S,
205 events: Fetcher,
206 /// Asked during a push which secrets have been allowed.
207 security: Option<Fetcher>,
208 /// Asked whether a workspace is on a plan, for its private storage.
209 billing: Option<Fetcher>,
210 /// Says which rulesets hold for a change to a branch or tag, and keeps
211 /// how they judged it (rules.rs). `None` where it is not deployed: the
212 /// old protection flag then holds on push.
213 work: Option<Fetcher>,
214 /// Told when a repository moves, for the tokens of agents at work on it.
215 identity: Option<Fetcher>,
216 /// What a free workspace's private repositories may hold.
217 free_private_bytes: i64,
218 /// Days a pull request's working copy is kept after it settles (forks.rs).
219 pub(crate) fork_days: u64,
220 /// The most a repository may hold (pack_limits.rs), and what happens
221 /// to a push too large to scan.
222 repo_limit: u64,
223 large_pushes: git_http::LargePushes,
224 /// Which git store namespace new repositories go in (shards.rs), and
225 /// the most each should hold (`GITSTORE_NAMESPACE_LIMITS`).
226 placement: shards::Placement,
227 limits: HashMap<String, u64>,
228 /// What isolates share: answers that list refs (refs_cache.rs).
229 shared: Option<Rc<shared::Shared>>,
230 /// Packs for fresh clones (pack_cache.rs); `None` without the bucket.
231 packs: Option<Rc<pack_cache::Packs>>,
232 /// What this request started that need not hold up its answer
233 /// (store.rs), handed to its `waitUntil` once it has answered.
234 deferred: Rc<store::Deferred>,
235}
236
237impl<S: GitStore> Repos<S> {
238 /// Records that the refs of the repository with this id changed, once
239 /// they have, so that the answers kept that list them go stale (see
240 /// refs_cache.rs). Everything that changes a repository's refs calls
241 /// this after it (`every_ref_writer_records_the_change` checks). A
242 /// failure is logged: the change itself happened, and what was kept
243 /// expires within `refs_cache::TTL_SECONDS` regardless.
244 pub(crate) async fn refs_moved(&self, repo_id: &str) {
245 if let Err(error) = self.registry.refs_moved(repo_id).await {
246 worker::console_error!("refs of {repo_id} changed but not recorded: {error}");
247 }
248 }
249
250 pub(crate) async fn publish<T: Serialize>(&self, event: NewEvent<T>) -> Result<()> {
251 g1t_kit::call(
252 &self.events,
253 "publish",
254 &Publish {
255 events: vec![event],
256 },
257 )
258 .await
259 }
260
261 /// Whether the viewer may read `repo`. A pull request's fork of a
262 /// private repository can be read by everyone who can read that
263 /// repository, so its members can review and check out the change, as
264 /// well as by whoever opened the pull request.
265 async fn may_read(&self, repo: &Repo, viewer: &Viewer) -> Result<bool> {
266 if can_read(repo, viewer) {
267 return Ok(true);
268 }
269 let Some(source_id) = &repo.fork_of else {
270 return Ok(false);
271 };
272 Ok(self
273 .registry
274 .by_id(source_id)
275 .await?
276 .is_some_and(|source| can_read(&source, viewer)))
277 }
278
279 /// `repo`, if there is one and the viewer may read it.
280 async fn visible(&self, repo: Option<Repo>, viewer: &Viewer) -> Result<Option<Repo>> {
281 Ok(match repo {
282 Some(repo) if self.may_read(&repo, viewer).await? => Some(repo),
283 _ => None,
284 })
285 }
286
287 /// Resolves a repo the viewer may read; private repos look missing.
288 pub(crate) async fn readable(&self, path: &RepoPath, viewer: &Viewer) -> Result<Option<Repo>> {
289 self.visible(self.registry.by_path(path).await?, viewer)
290 .await
291 }
292
293 async fn get(&self, a: GetArgs) -> Result<Outcome<Repo>> {
294 Ok(self
295 .readable(&a.path, &a.viewer)
296 .await?
297 .map_or_else(not_found, Outcome::Ok))
298 }
299
300 async fn get_by_id(&self, a: GetByIdArgs) -> Result<Outcome<Repo>> {
301 Ok(self
302 .visible(self.registry.by_id(&a.id).await?, &a.viewer)
303 .await?
304 .map_or_else(not_found, Outcome::Ok))
305 }
306
307 async fn update(&self, a: UpdateArgs) -> Result<Outcome<Repo>> {
308 let viewer = Some(a.actor.clone());
309 let Some(repo) = self.readable(&a.path, &viewer).await? else {
310 return Ok(not_found());
311 };
312 // Its details take Maintain; its protection, Admin; who can see it,
313 // Admin and the member privileges (below). See g1t_contracts::access.
314 let protection_changes = a.protected.is_some_and(|protected| protected != repo.protected);
315 let details_change = a.description.is_some() || a.website.is_some() || a.topics.is_some();
316 let mut needed = Vec::new();
317 if details_change || !protection_changes {
318 needed.push(Capability::ManageSettings);
319 }
320 if protection_changes {
321 needed.push(Capability::ManageProtection);
322 }
323 let full_name = format!("{}/{}", repo.namespace, repo.name);
324 if repo.fork_of.is_some() {
325 return Ok(Outcome::fail(FailureCode::Forbidden, access::needs(Capability::ManageSettings, &full_name)));
326 }
327 if let Some(missing) = needed.into_iter().find(|capability| !registry::can(&repo, &viewer, *capability)) {
328 return Ok(Outcome::fail(FailureCode::Forbidden, access::needs(missing, &full_name)));
329 }
330 if !a.actor.verified {
331 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
332 }
333 if let Some((code, message)) = lifecycle::archived_refusal(&repo) {
334 return Ok(Outcome::fail(code, message));
335 }
336 let description = match a.description {
337 Some(text) => Some(
338 text.trim()
339 .chars()
340 .take(MAX_DESCRIPTION_CHARS)
341 .collect::<String>(),
342 )
343 .filter(|text| !text.is_empty()),
344 None => repo.description.clone(),
345 };
346 let website = match a.website.as_deref() {
347 Some(text) => match clean_website(text) {
348 Ok(website) => website,
349 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
350 },
351 None => repo.website.clone(),
352 };
353 // Who can see it is an owner's to change, and a free workspace's
354 // storage may not take it private: see lifecycle.rs.
355 let wants_private = a.is_private.filter(|private| *private != repo.is_private);
356 if wants_private.is_some()
357 && let Err((code, message)) = lifecycle::admin_only(
358 lifecycle::Asker::on(&a.actor, &repo),
359 &repo.namespace,
360 "change the visibility of",
361 Capability::ChangeVisibility,
362 )
363 {
364 return Ok(Outcome::fail(code, message));
365 }
366 if let Some(private) = wants_private
367 && let Some(why) = lifecycle::visibility_refusal(&a.actor, &repo, private)
368 {
369 return Ok(Outcome::fail(FailureCode::Forbidden, why));
370 }
371 let is_private = repo.is_private;
372 let protected = a.protected.unwrap_or(repo.protected);
373 let topics = match &a.topics {
374 Some(topics) => match clean_topics(topics) {
375 Ok(topics) => topics,
376 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
377 },
378 None => repo.topics.clone(),
379 };
380 self.registry
381 .update(&repo.id, description.as_deref(), protected, &topics, website.as_deref())
382 .await?;
383 let updated = Repo {
384 description,
385 is_private,
386 protected,
387 topics,
388 website,
389 ..repo
390 };
391 // Whether the default branch takes only pull requests is now its
392 // branch protection ruleset's to say (work's rulesets.rs).
393 if let (Some(protected), Some(work)) = (a.protected, &self.work) {
394 #[derive(Serialize)]
395 struct RequirePullRequest<'a> {
396 repo: &'a Repo,
397 protected: bool,
398 actor: &'a User,
399 }
400 let set: Result<Outcome<bool>> =
401 g1t_kit::call(work, "set_requires_pull_request", &RequirePullRequest { repo: &updated, protected, actor: &a.actor }).await;
402 match set {
403 Ok(Outcome::Ok(_)) => {}
404 Ok(Outcome::Fail(failure)) => return Ok(Outcome::Fail(failure)),
405 Err(error) => return Err(error),
406 }
407 }
408 if let Some(private) = wants_private {
409 return self.change_visibility(updated, private, &a.actor, a.surface).await;
410 }
411 let visibility_changed = false;
412 // Search and anything else that shows the repository hears of it;
413 // a change of visibility is announced on its own as well, so that
414 // what was public stops being shown at once.
415 self.publish(NewEvent {
416 kind: "repo.updated",
417 source: SOURCE,
418 repo_id: Some(updated.id.clone()),
419 actor: Some(a.actor.id.clone()),
420 data: RepoUpdated {
421 repo_id: updated.id.clone(),
422 namespace: updated.namespace.clone(),
423 name: updated.name.clone(),
424 is_private,
425 visibility_changed,
426 },
427 })
428 .await?;
429 Ok(Outcome::Ok(updated))
430 }
431
432 /// The repository with this id, if it is not a fork, and its store.
433 async fn stored(&self, repo_id: &str) -> Result<Option<S::Repo>> {
434 match self.registry.by_id(repo_id).await? {
435 Some(repo) if repo.fork_of.is_none() => Ok(Some(self.store.open(&store_key(&repo)).await?)),
436 _ => Ok(None),
437 }
438 }
439
440 async fn list_files(&self, a: ListFilesArgs) -> Result<FileList> {
441 let Some(repo) = self.registry.by_id(&a.repo_id).await?.filter(|repo| repo.fork_of.is_none()) else {
442 return Ok(FileList::default());
443 };
444 let git = self.store.open(&store_key(&repo)).await?;
445 let head = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
446 listing::list(&git, None, &head, &a.skip_dirs, a.limit, a.links).await
447 }
448
449 /// A link to download a branch, tag or commit as an archive, good for
450 /// a few minutes with no session or token (archive_links.rs).
451 async fn archive_link(&self, a: ArchiveLinkArgs, sealer: Option<&g1t_secrets::Sealer>) -> Result<Outcome<ArchiveLink>> {
452 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
453 return Ok(not_found());
454 };
455 let Some(sealer) = sealer else {
456 return Ok(Outcome::fail(FailureCode::Invalid, "Archive downloads are not set up on this installation."));
457 };
458 let git = self.read_git(&repo).await?;
459 let git_ref = a.git_ref.filter(|git_ref| !git_ref.is_empty()).unwrap_or_else(|| repo.default_branch.clone());
460 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
461 return Ok(Outcome::fail(FailureCode::NotFound, "No such branch, tag or commit."));
462 };
463 let (token, expires) = archive_links::seal(sealer, &repo.id, &head.hash, now_ms());
464 Ok(Outcome::Ok(ArchiveLink { repo, commit: head.hash, token, expires_at: rfc3339(expires) }))
465 }
466
467 /// What an archive link's token lets its holder download.
468 async fn open_archive_token(&self, a: OpenArchiveTokenArgs, sealer: Option<&g1t_secrets::Sealer>) -> Result<Option<ArchiveGrant>> {
469 let Some(sealer) = sealer else {
470 return Ok(None);
471 };
472 let Some(repo) = self.registry.by_path(&a.path).await? else {
473 return Ok(None);
474 };
475 let Some(commit) = archive_links::open(sealer, &repo.id, &a.token, now_ms()) else {
476 return Ok(None);
477 };
478 Ok(Some(ArchiveGrant { repo_id: repo.id, namespace: repo.namespace, name: repo.name, commit }))
479 }
480
481 async fn changed_files(&self, a: ChangedFilesArgs) -> Result<FileList> {
482 let Some(git) = self.stored(&a.repo_id).await? else {
483 return Ok(FileList::default());
484 };
485 listing::list(&git, a.base.as_deref(), &a.head, &a.skip_dirs, a.limit, false).await
486 }
487
488 /// Branches and tags with their commits, for g1t's own services.
489 async fn refs_of(&self, a: RefsArgs) -> Result<Option<RepoRefs>> {
490 let Some(repo) = self.registry.by_id(&a.repo_id).await?.filter(|repo| repo.fork_of.is_none()) else {
491 return Ok(None);
492 };
493 let git = self.store.open(&store_key(&repo)).await?;
494 let access = git.access(Scope::Read).await?;
495 let refs = refs::heads_and_tags(refs::all(&access).await?)
496 .into_iter()
497 .map(|(name, commit)| GitRefEntry { name, commit })
498 .collect();
499 Ok(Some(RepoRefs { repo, refs }))
500 }
501
502 async fn raw_file(&self, a: RawFileArgs) -> Result<Option<RawFile>> {
503 use base64::Engine;
504 let Some(git) = self.stored(&a.repo_id).await? else {
505 return Ok(None);
506 };
507 Ok(git
508 .read_file(&a.git_ref, &a.path)
509 .await?
510 .filter(|bytes| bytes.len() <= a.max_bytes as usize)
511 .map(|bytes| RawFile { size: bytes.len() as u64, data: base64::engine::general_purpose::STANDARD.encode(bytes) }))
512 }
513
514 async fn raw_blobs(&self, a: RawBlobsArgs) -> Result<Vec<RawBlob>> {
515 use base64::Engine;
516 let Some(git) = self.stored(&a.repo_id).await? else {
517 return Ok(Vec::new());
518 };
519 let hashes: Vec<&String> = a.hashes.iter().take(MAX_READ_BLOBS).collect();
520 let mut out = Vec::with_capacity(hashes.len());
521 // A few at a time, as listing::read does: each is a round trip.
522 for group in hashes.chunks(8) {
523 let read = futures_util::future::try_join_all(group.iter().map(|hash| git.read_blob(hash))).await?;
524 for (hash, bytes) in group.iter().zip(read) {
525 let size = bytes.as_ref().map_or(0, |bytes| bytes.len() as u64);
526 let data = bytes
527 .filter(|bytes| bytes.len() <= a.max_bytes as usize)
528 .map(|bytes| base64::engine::general_purpose::STANDARD.encode(bytes));
529 out.push(RawBlob { hash: (*hash).clone(), size, data });
530 }
531 }
532 Ok(out)
533 }
534
535 async fn read_blobs(&self, a: ReadBlobsArgs) -> Result<Vec<BlobText>> {
536 let Some(git) = self.stored(&a.repo_id).await? else {
537 return Ok(Vec::new());
538 };
539 listing::read(&git, &a.hashes, a.max_bytes.min(MAX_TEXT_BYTES as u32)).await
540 }
541
542 async fn create(&self, a: CreateArgs) -> Result<Outcome<Repo>> {
543 if !a.owner.verified {
544 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
545 }
546 let name = a.name.trim().to_lowercase();
547 if !is_valid_repo_name(&name) {
548 return Ok(Outcome::fail(
549 FailureCode::Invalid,
550 "Use letters, digits, dots, hyphens and underscores only.",
551 ));
552 }
553 let namespace = a.namespace.trim().to_lowercase();
554 if namespace.is_empty() {
555 return Ok(Outcome::fail(
556 FailureCode::Invalid,
557 "Say which workspace to create the repository in.",
558 ));
559 }
560 let Some(role) = a.owner.role_in(&namespace) else {
561 return Ok(Outcome::fail(
562 FailureCode::Forbidden,
563 "You are not a member of that workspace.",
564 ));
565 };
566 // Who may create which: the workspace's member privileges. A
567 // workspace's own token acts as an owner would.
568 let role = if a.owner.kind == PrincipalKind::Workspace { Role::Owner } else { role };
569 if let Some(why) = a.owner.privileges_in(&namespace).creation_refusal(role, a.is_private, &namespace) {
570 return Ok(Outcome::fail(FailureCode::Forbidden, why));
571 }
572 let path = RepoPath { namespace, name };
573 match self.registry.by_path_any(&path).await? {
574 Some((_, None)) => {
575 return Ok(Outcome::fail(
576 FailureCode::Conflict,
577 "That workspace already has a repository with that name.",
578 ));
579 }
580 Some((_, Some(_))) => {
581 return Ok(Outcome::fail(
582 FailureCode::Conflict,
583 format!(
584 "{}/{} 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.",
585 path.namespace, path.name
586 ),
587 ));
588 }
589 None => {}
590 }
591 // With a credential (a GitHub App installation's token), everything
592 // is copied: every branch and tag. See copy.rs.
593 let mut credentialed = None;
594 if let (Some(url), Some(token)) = (a.import_url.as_deref(), a.import_token.as_deref()) {
595 let Some(url) = import::clean_url(url) else {
596 return Ok(Outcome::fail(FailureCode::Invalid, "That is not an https repository address."));
597 };
598 let source = copy::Endpoint::github(&url, token);
599 match copy::probe(&source).await? {
600 Ok(advertised) => credentialed = Some((source, advertised)),
601 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
602 }
603 }
604 // An import is fetched before anything is created, so that an
605 // address that does not work leaves nothing behind.
606 let mut imported = None;
607 if let Some(url) = a
608 .import_url
609 .as_deref()
610 .map(str::trim)
611 .filter(|url| !url.is_empty() && credentialed.is_none())
612 {
613 let Some(url) = import::clean_url(url) else {
614 return Ok(Outcome::fail(
615 FailureCode::Invalid,
616 "Give the https address of a public repository, such as https://github.com/owner/repo.",
617 ));
618 };
619 let remote = match import::discover(&url).await? {
620 Ok(remote) => remote,
621 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
622 };
623 imported = Some((remote, url));
624 }
625 let now = now_ms();
626 let repo = Repo {
627 id: new_id("rep", now),
628 namespace: path.namespace,
629 name: path.name,
630 description: a
631 .description
632 .map(|text| text.trim().to_owned())
633 .filter(|text| !text.is_empty()),
634 is_private: a.is_private,
635 owner_id: a.owner.id.clone(),
636 default_branch: imported
637 .as_ref()
638 .map(|(remote, _)| remote.branch.clone())
639 .or_else(|| credentialed.as_ref().and_then(|(_, advertised)| advertised.default_branch()))
640 .unwrap_or_else(|| "main".to_owned()),
641 fork_of: None,
642 protected: false,
643 created_at: rfc3339(now),
644 topics: Vec::new(),
645 website: None,
646 archived_at: None,
647 };
648 let namespace = match self.place(&repo).await? {
649 Ok(namespace) => namespace,
650 Err(unplaced) => return Ok(Outcome::fail(FailureCode::Conflict, unplaced.message())),
651 };
652 self.registry
653 .claim_store_key(&repo, namespace.as_deref(), &self.store.default_namespace())
654 .await?;
655 self.store
656 .create(
657 &store_key(&repo),
658 repo.description.as_deref(),
659 &repo.default_branch,
660 )
661 .await?;
662 self.registry.insert(&repo).await?;
663 // A repository that was transferred away from this path stops
664 // redirecting here.
665 self.registry
666 .drop_redirect(&RepoPath {
667 namespace: repo.namespace.clone(),
668 name: repo.name.clone(),
669 })
670 .await?;
671 // Every branch and tag the import made, announced as pushes.
672 let mut pushed: Vec<(String, String)> = Vec::new();
673 // A public repository, read with no credential: every branch and
674 // tag is copied too, the default branch the one its HEAD names.
675 if let Some((_, url)) = imported {
676 let access = self
677 .store
678 .open(&store_key(&repo))
679 .await?
680 .access(Scope::Write)
681 .await?;
682 let target = copy::Endpoint::bearer(&access.remote, &access.token);
683 let copied = copy::copy(&copy::Endpoint::anonymous(&url), &target).await?;
684 self.refs_moved(&repo.id).await;
685 match copied {
686 Ok(copied) => pushed = copy::import_pushes(&copied.updated, &repo.default_branch),
687 Err(reason) => {
688 self.registry.remove(&repo.id).await?;
689 return Ok(Outcome::fail(
690 FailureCode::Invalid,
691 format!("The repository could not be stored: {reason}"),
692 ));
693 }
694 }
695 }
696 if let Some((source, _)) = credentialed {
697 let access = self
698 .store
699 .open(&store_key(&repo))
700 .await?
701 .access(Scope::Write)
702 .await?;
703 let target = copy::Endpoint::bearer(&access.remote, &access.token);
704 let copied = copy::copy(&source, &target).await?;
705 self.refs_moved(&repo.id).await;
706 match copied {
707 Ok(copied) => pushed = copy::import_pushes(&copied.updated, &repo.default_branch),
708 Err(reason) => {
709 self.registry.remove(&repo.id).await?;
710 return Ok(Outcome::fail(
711 FailureCode::Invalid,
712 format!("The repository could not be copied: {reason}"),
713 ));
714 }
715 }
716 }
717 // Whoever creates a repository is an Admin of it, as a role given
718 // to them on it, whatever the workspace's base permission.
719 if a.owner.kind == PrincipalKind::User
720 && let Some(identity) = &self.identity
721 {
722 let granted: Result<bool> = g1t_kit::call(
723 identity,
724 "grant_creator",
725 &g1t_contracts::members::GrantCreatorArgs {
726 repo_id: repo.id.clone(),
727 namespace: repo.namespace.clone(),
728 name: repo.name.clone(),
729 user_id: a.owner.id.clone(),
730 },
731 )
732 .await;
733 if let Err(error) = granted {
734 worker::console_error!("creator of {} not given Admin: {error}", repo.id);
735 }
736 }
737 self.publish(NewEvent {
738 kind: "repo.created",
739 source: SOURCE,
740 repo_id: Some(repo.id.clone()),
741 actor: Some(a.owner.id),
742 data: RepoCreated {
743 repo_id: repo.id.clone(),
744 namespace: repo.namespace.clone(),
745 name: repo.name.clone(),
746 is_private: repo.is_private,
747 },
748 })
749 .await?;
750 for (git_ref, head) in &pushed {
751 self.publish_push(&repo, git_ref, None, head, None, None).await?;
752 }
753 Ok(Outcome::Ok(repo))
754 }
755
756 /// Where a workspace keeps its data, asked of identity only when an EU
757 /// namespace is configured: without one, every workspace's
758 /// repositories go anywhere and identity is never asked.
759 async fn residency_of(&self, workspace: &str) -> Result<shards::Residency> {
760 if self.placement.eu.is_none() {
761 return Ok(shards::Residency::Anywhere);
762 }
763 let Some(identity) = &self.identity else {
764 return Ok(shards::Residency::Anywhere);
765 };
766 let residency: Option<g1t_contracts::identity::DataResidency> = g1t_kit::call(
767 identity,
768 "workspace_residency",
769 &g1t_contracts::identity::SlugArgs { slug: workspace.to_owned() },
770 )
771 .await?;
772 Ok(match residency {
773 Some(g1t_contracts::identity::DataResidency::Eu) => shards::Residency::Eu,
774 _ => shards::Residency::Anywhere,
775 })
776 }
777
778 /// How each bound namespace stands (namespaces.rs).
779 async fn standings(&self) -> Result<Vec<namespaces::Standing>> {
780 let bound = self.store.namespaces();
781 let default = self.store.default_namespace();
782 let now = now_ms();
783 let config = namespaces::Configured {
784 bound: &bound,
785 default: &default,
786 placement: &self.placement,
787 limits: &self.limits,
788 on_fallback: &|namespace| self.store.on_fallback(&shards::compose(Some(namespace), "x", &default)),
789 writable: &|namespace| self.store.writable(namespace),
790 breaker_open: &|namespace| resilience::open_now(namespace, now),
791 };
792 let (held, recent) = futures_util::future::join(namespaces::held(&self.registry.db), namespaces::recent(&self.registry.db, now)).await;
793 Ok(namespaces::standings(&config, &held?, &recent?))
794 }
795
796 /// The namespace a new repository goes in (shards.rs): its workspace's
797 /// residency, then how each namespace stands, read only when there is
798 /// a choice to make. `Ok(None)` for the default.
799 async fn place(&self, repo: &Repo) -> Result<std::result::Result<Option<String>, shards::Unplaced>> {
800 let residency = self.residency_of(&repo.namespace).await?;
801 let bound = self.store.namespaces();
802 let loads = if residency == shards::Residency::Anywhere && !self.placement.needs_loads(&bound) {
803 // One namespace to choose from at most: nothing to read.
804 bound
805 .iter()
806 .map(|namespace| shards::Load {
807 namespace: namespace.clone(),
808 bound: true,
809 writable: self.store.writable(namespace),
810 ..shards::Load::default()
811 })
812 .collect()
813 } else {
814 let default = self.store.default_namespace();
815 let now = now_ms();
816 let config = namespaces::Configured {
817 bound: &bound,
818 default: &default,
819 placement: &self.placement,
820 limits: &self.limits,
821 on_fallback: &|namespace| self.store.on_fallback(&shards::compose(Some(namespace), "x", &default)),
822 writable: &|namespace| self.store.writable(namespace),
823 breaker_open: &|namespace| resilience::open_now(namespace, now),
824 };
825 namespaces::loads(&self.registry.db, &config, now).await?
826 };
827 Ok(self.placement.choose(&repo.id, residency, &loads))
828 }
829
830 /// `storage_options`: what a workspace may choose about where its
831 /// repositories are kept.
832 fn storage_options(&self) -> StorageOptions {
833 let bound = self.store.namespaces();
834 StorageOptions { eu_available: self.placement.eu_available(&bound, |namespace| self.store.writable(namespace)) }
835 }
836
837 async fn tree(&self, a: TreeArgs) -> Result<Outcome<TreeView>> {
838 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
839 return Ok(not_found());
840 };
841 let git = self.read_git(&repo).await?;
842 let git_ref = a
843 .git_ref
844 .clone()
845 .unwrap_or_else(|| repo.default_branch.clone());
846
847 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
848 // An unknown ref is an error; a repo with no commits is just empty.
849 if a.git_ref.is_some() {
850 return Ok(Outcome::fail(
851 FailureCode::NotFound,
852 "No such branch, tag or commit.",
853 ));
854 }
855 return Ok(Outcome::Ok(TreeView {
856 repo,
857 git_ref,
858 path: a.tree_path,
859 head: None,
860 entries: Vec::new(),
861 readme: None,
862 }));
863 };
864
865 let no_directory = || Outcome::fail(FailureCode::NotFound, "No such directory.");
866 let mut entries = git.read_tree(&head.tree_hash).await?;
867 for segment in a.tree_path.split('/').filter(|segment| !segment.is_empty()) {
868 let next = entries.as_ref().and_then(|entries| {
869 entries
870 .iter()
871 .find(|entry| entry.name == segment && entry.kind == EntryKind::Tree)
872 });
873 let Some(next) = next else {
874 return Ok(no_directory());
875 };
876 entries = git.read_tree(&next.hash).await?;
877 }
878 let Some(mut entries) = entries else {
879 return Ok(no_directory());
880 };
881 // Directories first, then by name.
882 entries.sort_by(|a, b| {
883 (b.kind == EntryKind::Tree)
884 .cmp(&(a.kind == EntryKind::Tree))
885 .then_with(|| a.name.cmp(&b.name))
886 });
887
888 let readme_entry = entries
889 .iter()
890 .find(|entry| entry.kind == EntryKind::Blob && is_readme(&entry.name));
891 let readme = match readme_entry {
892 Some(entry) => git.read_blob(&entry.hash).await?.map(|bytes| Readme {
893 name: entry.name.clone(),
894 text: text_of(bytes),
895 }),
896 None => None,
897 };
898 Ok(Outcome::Ok(TreeView {
899 repo,
900 git_ref,
901 path: a.tree_path,
902 head: Some(head),
903 entries,
904 readme,
905 }))
906 }
907
908 async fn blob(&self, a: BlobArgs) -> Result<Outcome<BlobView>> {
909 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
910 return Ok(not_found());
911 };
912 let bytes = if a.file_path.is_empty() {
913 None
914 } else {
915 let git = self.read_git(&repo).await?;
916 git.read_file(&a.git_ref, &a.file_path).await?
917 };
918 let Some(bytes) = bytes else {
919 return Ok(Outcome::fail(FailureCode::NotFound, "No such file."));
920 };
921 Ok(Outcome::Ok(BlobView {
922 repo,
923 git_ref: a.git_ref,
924 path: a.file_path,
925 size: bytes.len() as u64,
926 text: text_of(bytes),
927 }))
928 }
929
930 async fn blame(&self, a: BlameArgs) -> Result<Outcome<Blame>> {
931 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
932 return Ok(not_found());
933 };
934 let git = self.read_git(&repo).await?;
935 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
936 Ok(match blame::blame(&git, &git_ref, &a.file_path).await? {
937 Some(blame) => Outcome::Ok(blame),
938 None => not_found(),
939 })
940 }
941
942 async fn log(&self, a: LogArgs) -> Result<Outcome<Vec<Commit>>> {
943 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
944 return Ok(not_found());
945 };
946 let git = self.read_git(&repo).await?;
947 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
948 Ok(Outcome::Ok(git.log(&git_ref, a.limit).await?))
949 }
950
951 /// Which commit last changed each entry of a directory. A finished
952 /// answer is kept in this colo's cache by repository, head commit and
953 /// path: a commit's history never changes, so it is good for as long as
954 /// it is kept. Every walk also keeps its progress by ref and path
955 /// (last_commits.rs), so the next call goes on from it: from where it
956 /// stopped, or for a new head, only back to the old one.
957 async fn last_commits(&self, a: g1t_contracts::repos::LastCommitsArgs) -> Result<Outcome<g1t_contracts::repos::LastCommits>> {
958 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
959 return Ok(not_found());
960 };
961 let git = self.read_git(&repo).await?;
962 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
963 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
964 return Ok(Outcome::fail(FailureCode::NotFound, "No such branch, tag or commit."));
965 };
966 // v2: v1 kept walks cut short at 300 commits as if finished.
967 let key = format!(
968 "https://last-commits.g1t.internal/v2/{}/{}/{}",
969 repo.id,
970 head.hash,
971 a.tree_path.split('/').map(urlencoding_segment).collect::<Vec<_>>().join("/")
972 );
973 let cache = worker::Cache::default();
974 if let Ok(Some(mut kept)) = cache.get(key.as_str(), false).await {
975 if let Ok(found) = kept.json::<g1t_contracts::repos::LastCommits>().await {
976 return Ok(Outcome::Ok(found));
977 }
978 }
979 let memo = last_commits::Memo::new(&repo.id, &git_ref, &a.tree_path);
980 let shared = self.shared.as_deref();
981 let progress = memo.get(shared).await;
982 // Asked with a budget, the walk stops past it; without one, past
983 // LAST_COMMITS_WALK_MS, well before the caller's waitUntil ends.
984 let started = worker::Date::now().as_millis();
985 let budget = a.budget_ms.unwrap_or(LAST_COMMITS_WALK_MS);
986 let out_of_time = move || worker::Date::now().as_millis().saturating_sub(started) > budget;
987 let walk = last_commits::last_commits(&git, &head.hash, &a.tree_path, progress, &out_of_time, last_commits::MAX_READS).await?;
988 if walk.reads > 0 {
989 memo.keep(shared, &walk.progress).await;
990 }
991 let settled = walk.progress.settled();
992 let found = g1t_contracts::repos::LastCommits { complete: walk.progress.complete(), entries: walk.progress.found };
993 if !settled {
994 return Ok(Outcome::Ok(found));
995 }
996 if let Ok(mut response) = worker::Response::from_json(&found) {
997 let _ = response.headers_mut().set("cache-control", "max-age=604800");
998 let _ = cache.put(key.as_str(), response).await;
999 }
1000 Ok(Outcome::Ok(found))
1001 }
1002
1003 /// How far each branch head has moved from the default branch's head,
1004 /// in one call (drift.rs). Each count is kept in this colo's cache by
1005 /// repository and the pair of hashes, for good: neither history can
1006 /// change. A head that moved is the only one walked. A failed read is
1007 /// not kept, and "too far to count" only for a day.
1008 async fn branch_drift(&self, a: BranchDriftArgs) -> Result<Outcome<BranchDrifts>> {
1009 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
1010 return Ok(not_found());
1011 };
1012 if !store::is_commit_hash(&a.base) {
1013 return Ok(Outcome::fail(FailureCode::Invalid, "The default branch's head is a full commit hash."));
1014 }
1015 let heads: Vec<String> = a.heads.into_iter().take(MAX_DRIFT_HEADS).collect();
1016 let git = self.read_git(&repo).await?;
1017 // v2: answers kept before 2026-10-09 said "no count" for every
1018 // branch whose default branch took a merge since it left (drift.rs).
1019 let key = |head: &str| format!("https://drift.g1t.internal/v2/{}/{}/{head}", repo.id, a.base);
1020 let cache = worker::Cache::default();
1021 let (base, kept) = futures_util::future::join(
1022 git.log(&a.base, 1),
1023 futures_util::future::join_all(heads.iter().map(|head| {
1024 let (cache, url) = (&cache, key(head));
1025 async move {
1026 if !store::is_commit_hash(head) {
1027 return None;
1028 }
1029 let mut found = cache.get(url.as_str(), false).await.ok()??;
1030 found.json::<BranchDrift>().await.ok()
1031 }
1032 })),
1033 )
1034 .await;
1035 let missing: Vec<String> = heads
1036 .iter()
1037 .zip(&kept)
1038 .filter(|(head, kept)| kept.is_none() && store::is_commit_hash(head))
1039 .map(|(head, _)| head.clone())
1040 .collect();
1041 let measured = drift::measure(&git, &a.base, &missing).await;
1042 let mut fresh: HashMap<String, BranchDrift> = HashMap::new();
1043 for (head, found) in missing.into_iter().zip(measured) {
1044 let answer = BranchDrift { head: head.clone(), commit: found.commit, drift: found.drift };
1045 if found.settled
1046 && let Ok(mut response) = worker::Response::from_json(&answer)
1047 {
1048 // A count is kept for good; "too far to count" for a day, so
1049 // a change to how far a walk goes reaches it.
1050 let max_age = if answer.drift.is_some() { "public, max-age=31536000, immutable" } else { "public, max-age=86400" };
1051 let _ = response.headers_mut().set("cache-control", max_age);
1052 let _ = cache.put(key(&head).as_str(), response).await;
1053 }
1054 fresh.insert(head, answer);
1055 }
1056 let branches = heads
1057 .iter()
1058 .zip(kept)
1059 .map(|(head, kept)| {
1060 kept.or_else(|| fresh.get(head).cloned())
1061 .unwrap_or_else(|| BranchDrift { head: head.clone(), commit: None, drift: None })
1062 })
1063 .collect();
1064 Ok(Outcome::Ok(BranchDrifts { base: base.ok().and_then(|log| log.into_iter().next()), branches }))
1065 }
1066
1067 /// The repository's tags, newest commit first, at most 100.
1068 async fn tags(&self, a: g1t_contracts::repos::TagsArgs) -> Result<Outcome<Vec<g1t_contracts::repos::Tag>>> {
1069 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
1070 return Ok(not_found());
1071 };
1072 // Kept until the refs move, as the branch list is (store.rs): listing
1073 // the refs is a round trip to the store on every call otherwise.
1074 let version = refs_cache::usable(registry::refs_state(&repo.id), now_ms()).filter(|_| !self.store.on_fallback(&store_key(&repo)));
1075 let kept_at = version.map(|version| format!("https://tags.g1t.internal/{}/{version}", repo.id));
1076 if let Some(url) = &kept_at
1077 && let Ok(Some(mut kept)) = worker::Cache::default().get(url.as_str(), false).await
1078 && let Ok(tags) = kept.json::<Vec<g1t_contracts::repos::Tag>>().await
1079 {
1080 return Ok(Outcome::Ok(tags));
1081 }
1082 let (tags, complete) = self.read_tags(&repo).await?;
1083 if complete
1084 && let Some(url) = &kept_at
1085 && let Ok(mut response) = worker::Response::from_json(&tags)
1086 {
1087 let _ = response.headers_mut().set("cache-control", "public, max-age=300");
1088 let _ = worker::Cache::default().put(url.as_str(), response).await;
1089 }
1090 Ok(Outcome::Ok(tags))
1091 }
1092
1093 /// The tags, and whether every one's commit was read (only then kept).
1094 async fn read_tags(&self, repo: &Repo) -> Result<(Vec<g1t_contracts::repos::Tag>, bool)> {
1095 let git = self.store.open(&store_key(repo)).await?;
1096 let access = git.access(Scope::Read).await?;
1097 let named: Vec<(String, String)> = refs::heads_and_tags(refs::all(&access).await?)
1098 .into_iter()
1099 .filter_map(|(name, hash)| name.strip_prefix("refs/tags/").map(|tag| (tag.to_owned(), hash)))
1100 .collect();
1101 let read = self.read_git(repo).await?;
1102 let commits = futures_util::future::join_all(named.iter().take(MAX_TAGS_READ).map(|(_, hash)| read.log(hash, 1))).await;
1103 let complete = commits.iter().all(Result::is_ok);
1104 let mut tags: Vec<g1t_contracts::repos::Tag> = named
1105 .into_iter()
1106 .zip(commits.into_iter().map(|found| found.ok().and_then(|list| list.into_iter().next())).chain(std::iter::repeat(None)))
1107 .map(|((name, _), commit)| g1t_contracts::repos::Tag { name, commit })
1108 .collect();
1109 tags.sort_by(|a, b| {
1110 let at = |tag: &g1t_contracts::repos::Tag| tag.commit.as_ref().map(|c| c.authored_at.clone()).unwrap_or_default();
1111 at(b).cmp(&at(a)).then_with(|| b.name.cmp(&a.name))
1112 });
1113 tags.truncate(MAX_TAGS_READ);
1114 Ok((tags, complete))
1115 }
1116
1117 /// The repository's branches, default branch first.
1118 async fn branches(&self, a: BranchesArgs) -> Result<Outcome<Vec<Branch>>> {
1119 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
1120 return Ok(not_found());
1121 };
1122 let mut branches = self.read_git(&repo).await?.branches().await?;
1123 branches.sort_by_key(|branch| branch.name != repo.default_branch);
1124 Ok(Outcome::Ok(branches))
1125 }
1126
1127 /// Whether a pull request's source lacks commits that the branch it
1128 /// would merge into has.
1129 async fn behind(&self, a: BehindArgs) -> Result<bool> {
1130 let Some(source) = self.registry.by_id(&a.source_id).await? else {
1131 return Ok(false);
1132 };
1133 let target = match &source.fork_of {
1134 Some(id) => self.registry.by_id(id).await?,
1135 None => Some(source.clone()),
1136 };
1137 let Some(target) = target else {
1138 return Ok(false);
1139 };
1140 let branch = a.branch.unwrap_or_else(|| target.default_branch.clone());
1141 let target_branch = a.target_branch.unwrap_or_else(|| target.default_branch.clone());
1142 let target_head = self
1143 .read_git(&target)
1144 .await?
1145 .log(&target_branch, 1)
1146 .await?
1147 .into_iter()
1148 .next()
1149 .map(|commit| commit.hash);
1150 let Some(target_head) = target_head else {
1151 return Ok(false);
1152 };
1153 let source_git = self.read_git(&source).await?;
1154 let history = source_git.log(&branch, MAX_ANCESTRY).await?;
1155 if history.is_empty() {
1156 return Ok(false);
1157 }
1158 Ok(!descends_from(&source_git, &history, &target_head).await?)
1159 }
1160
1161 /// The files a pull request's source and the default branch it would
1162 /// merge into each changed since they last agreed. Where the two lists
1163 /// share no file, the merge cannot conflict; where they do, it may.
1164 async fn divergence(&self, a: BehindArgs) -> Result<Option<Divergence>> {
1165 let Some(source) = self.registry.by_id(&a.source_id).await? else {
1166 return Ok(None);
1167 };
1168 let target = match &source.fork_of {
1169 Some(id) => self.registry.by_id(id).await?,
1170 None => Some(source.clone()),
1171 };
1172 let Some(target) = target else {
1173 return Ok(None);
1174 };
1175 let branch = a.branch.unwrap_or_else(|| target.default_branch.clone());
1176 let target_branch = a.target_branch.unwrap_or_else(|| target.default_branch.clone());
1177 let source_git = self.read_git(&source).await?;
1178 let target_git = self.read_git(&target).await?;
1179 // The target's side is the same for every pull request into it, and
1180 // worked out once per head (coalesce.rs).
1181 let (history, side) = futures_util::future::try_join(
1182 source_git.log(&branch, MAX_ANCESTRY),
1183 self.target_side(&target, &target_git, &target_branch),
1184 )
1185 .await?;
1186 let target_history = &side.history;
1187 let (Some(head), Some(base)) = (history.first(), target_history.first()) else {
1188 return Ok(None);
1189 };
1190 let behind = !descends_from(&source_git, &history, &base.hash).await?;
1191 let merge_base = nearest_ancestor_in(&source_git, &history, &side.shared).await?;
1192 let mut divergence = Divergence {
1193 head: head.hash.clone(),
1194 base: base.hash.clone(),
1195 merge_base: merge_base.clone(),
1196 behind,
1197 ..Divergence::default()
1198 };
1199 let merge_base_tree = match &merge_base {
1200 Some(hash) => target_history
1201 .iter()
1202 .find(|commit| commit.hash == *hash)
1203 .map(|commit| commit.tree_hash.clone()),
1204 None => None,
1205 };
1206 let Some(merge_base_tree) = merge_base_tree else {
1207 // No common history to compare from: say nothing is known.
1208 divergence.truncated = true;
1209 return Ok(Some(divergence));
1210 };
1211 let (ours, truncated_ours) =
1212 diff::changed_paths(&source_git, Some(&merge_base_tree), &head.tree_hash).await?;
1213 divergence.ours = ours;
1214 divergence.truncated = truncated_ours;
1215 if behind {
1216 let now = now_ms();
1217 let key = (target.id.clone(), merge_base_tree.clone(), base.tree_hash.clone());
1218 let (theirs, truncated_theirs) = match THEIRS.with(|memo| memo.borrow().get(&key, now)) {
1219 Some(kept) => kept,
1220 None => {
1221 let found = diff::changed_paths(&target_git, Some(&merge_base_tree), &base.tree_hash).await?;
1222 THEIRS.with(|memo| memo.borrow_mut().put(key, found.clone(), now));
1223 found
1224 }
1225 };
1226 divergence.theirs = theirs;
1227 divergence.truncated |= truncated_theirs;
1228 }
1229 Ok(Some(divergence))
1230 }
1231
1232 /// A target branch's history from its head, worked out once per head
1233 /// for every pull request asking about it (coalesce.rs). The head is
1234 /// read under the refs version; the history by its hash, which the
1235 /// object cache keeps for good.
1236 async fn target_side<R: GitRepo>(&self, target: &Repo, git: &R, branch: &str) -> Result<Rc<coalesce::TargetSide>> {
1237 let now = now_ms();
1238 let key = refs_cache::usable(registry::refs_state(&target.id), now)
1239 .map(|version| (target.id.clone(), branch.to_owned(), version));
1240 if let Some(key) = &key
1241 && let Some(side) = TARGETS.with(|memo| memo.borrow().get(key, now))
1242 {
1243 return Ok(side);
1244 }
1245 let history = match git.log(branch, 1).await?.first() {
1246 Some(head) => git.log(&head.hash, MAX_ANCESTRY).await?,
1247 None => Vec::new(),
1248 };
1249 let side = coalesce::TargetSide::new(history);
1250 if let Some(key) = key {
1251 TARGETS.with(|memo| memo.borrow_mut().put(key, side.clone(), now));
1252 }
1253 Ok(side)
1254 }
1255
1256 async fn head(&self, a: HeadArgs) -> Result<Option<String>> {
1257 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
1258 return Ok(None);
1259 };
1260 let branch = if a.branch.is_empty() { &repo.default_branch } else { &a.branch };
1261 let git = self.read_git(&repo).await?;
1262 Ok(git
1263 .log(branch, 1)
1264 .await?
1265 .into_iter()
1266 .next()
1267 .map(|commit| commit.hash))
1268 }
1269
1270 async fn delete_branch(&self, a: DeleteBranchArgs) -> Result<Outcome<bool>> {
1271 if !deletable_branch(&a.branch, a.head.as_deref()) {
1272 return Ok(Outcome::fail(
1273 FailureCode::Forbidden,
1274 "Only branches g1t made for itself can be deleted this way.",
1275 ));
1276 }
1277 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
1278 return Ok(not_found());
1279 };
1280 if a.branch == repo.default_branch {
1281 return Ok(Outcome::fail(FailureCode::Forbidden, "The default branch is never deleted."));
1282 }
1283 let repo = match self.unpaused(repo).await? {
1284 Ok(repo) => repo,
1285 Err((code, message)) => return Ok(Outcome::fail(code, message)),
1286 };
1287 self.live(&repo).await?;
1288 let git = self.store.open(&store_key(&repo)).await?;
1289 let Some(old) = git
1290 .branches()
1291 .await?
1292 .into_iter()
1293 .find(|branch| branch.name == a.branch)
1294 .map(|branch| branch.hash)
1295 else {
1296 return Ok(Outcome::Ok(false));
1297 };
1298 // Moved since the caller looked: someone else's commits are on it.
1299 if a.head.as_deref().is_some_and(|head| head != old) {
1300 return Ok(Outcome::fail(FailureCode::Conflict, format!("{} moved, so it was left alone.", a.branch)));
1301 }
1302 let access = git.access(Scope::Write).await?;
1303 let deleted = land::delete_ref(&access, &a.branch, &old).await?;
1304 self.refs_moved(&repo.id).await;
1305 if let Err(reason) = deleted {
1306 return Ok(Outcome::fail(
1307 FailureCode::Conflict,
1308 format!("{} could not be deleted: {reason}", a.branch),
1309 ));
1310 }
1311 Ok(Outcome::Ok(true))
1312 }
1313
1314 async fn fork_for_pull(&self, a: ForkArgs) -> Result<Outcome<Repo>> {
1315 let viewer = Some(a.actor.clone());
1316 let Some(source) = self
1317 .registry
1318 .by_id(&a.source_id)
1319 .await?
1320 .filter(|repo| can_read(repo, &viewer))
1321 else {
1322 return Ok(not_found());
1323 };
1324 if let Some((code, message)) = lifecycle::read_only_refusal(&source) {
1325 return Ok(Outcome::fail(code, message));
1326 }
1327 // Its working copy is made in its namespace: not while it moves.
1328 let source = match self.unpaused(source).await? {
1329 Ok(source) => source,
1330 Err((code, message)) => return Ok(Outcome::fail(code, message)),
1331 };
1332 let now = now_ms();
1333 let fork = Repo {
1334 id: new_id("rep", now),
1335 namespace: PULLS_NAMESPACE.to_owned(),
1336 name: a.pull_id.clone(),
1337 description: None,
1338 // A fork is exactly as visible as the repo it came from.
1339 is_private: source.is_private,
1340 owner_id: a.actor.id.clone(),
1341 default_branch: source.default_branch.clone(),
1342 fork_of: Some(source.id.clone()),
1343 protected: false,
1344 created_at: rfc3339(now),
1345 topics: Vec::new(),
1346 website: None,
1347 archived_at: None,
1348 };
1349 // Artifacts forks within a namespace: the copy goes where its
1350 // repository is.
1351 let (namespace, _) = store::locate(&store_key(&source));
1352 self.registry
1353 .claim_store_key(&fork, Some(&namespace), &self.store.default_namespace())
1354 .await?;
1355 self.store
1356 .open(&store_key(&source))
1357 .await?
1358 .fork(&store_key(&fork))
1359 .await?;
1360 self.registry.insert(&fork).await?;
1361 self.publish(NewEvent {
1362 kind: "repo.forked",
1363 source: SOURCE,
1364 repo_id: Some(source.id.clone()),
1365 actor: Some(a.actor.id),
1366 data: RepoForked {
1367 repo_id: fork.id.clone(),
1368 source_repo_id: source.id,
1369 pull_id: a.pull_id,
1370 },
1371 })
1372 .await?;
1373 Ok(Outcome::Ok(fork))
1374 }
1375
1376 async fn git_access(&self, a: GitAccessArgs) -> Result<Outcome<GitAccess>> {
1377 let found = self.registry.by_path(&a.path).await?;
1378 Ok(match self.authorize_git(&a.path, &a.viewer, a.service, found).await? {
1379 Outcome::Ok(repo) => {
1380 self.live(&repo).await?;
1381 let write = a.service == GitService::ReceivePack;
1382 if write {
1383 // A push with this credential would not pass through
1384 // here, so nothing that lists the refs is kept until it
1385 // has expired (see refs_cache.rs).
1386 let until = now_ms() + store::CREDENTIAL_LIFE_MS + 60_000;
1387 if let Err(error) = self.registry.refs_open(&repo.id, until).await {
1388 // Before the column exists nothing is kept anyway.
1389 if registry::refs_state(&repo.id).is_some() {
1390 return Err(error);
1391 }
1392 }
1393 }
1394 let scope = if write { Scope::Write } else { Scope::Read };
1395 Outcome::Ok(self.store.handout(&store_key(&repo), scope).await?)
1396 }
1397 Outcome::Fail(failure) => Outcome::Fail(failure),
1398 })
1399 }
1400
1401 /// The repository at `path` (`found`, as just read), if the viewer may
1402 /// use `service` on it: fetch from it, or push to it. A push to a path
1403 /// with nothing there makes the repository, in a workspace the pusher
1404 /// belongs to.
1405 async fn authorize_git(
1406 &self,
1407 path: &RepoPath,
1408 viewer: &Viewer,
1409 service: GitService,
1410 found: Option<Repo>,
1411 ) -> Result<Outcome<Repo>> {
1412 let mut a = GitAccessArgs {
1413 path: path.clone(),
1414 viewer: viewer.clone(),
1415 service,
1416 };
1417 let write = a.service == GitService::ReceivePack;
1418 // An access token: pushing needs code:write, reading a private
1419 // repository code:read. A public repository reads as it would for
1420 // anyone. Which repositories a token reaches is its owner's, checked
1421 // below as for anyone.
1422 if let Some(access) = a.viewer.as_ref().and_then(|user| user.token.as_deref()).cloned() {
1423 // A workflow job's token, and a deploy key, reach their own
1424 // repository only; a job's also the working copies of that
1425 // repository's pull requests, where their heads are.
1426 let name = format!("{}/{}", path.namespace, path.name);
1427 let source = match found.as_ref().and_then(|repo| repo.fork_of.as_deref()) {
1428 Some(source_id) if g1t_contracts::scopes::decide_repo(&access, &name).is_some() => self
1429 .registry
1430 .by_id(source_id)
1431 .await?
1432 .map(|source| format!("{}/{}", source.namespace, source.name)),
1433 _ => None,
1434 };
1435 let public = found.as_ref().is_some_and(|repo| !repo.is_private);
1436 if let Some(why) = git_token_refusal(&access, &name, source.as_deref(), write, public, found.is_some()) {
1437 return Ok(Outcome::fail(FailureCode::Forbidden, format!("{why}\n")));
1438 }
1439 if !write && !access.allows(g1t_contracts::scopes::Scope::CodeRead) {
1440 a.viewer = None;
1441 }
1442 }
1443
1444 // Anonymous callers are asked to authenticate whether or not the repo
1445 // exists, so private repos cannot be told apart from missing ones.
1446 let denied = || match &a.viewer {
1447 Some(_) => not_found(),
1448 None => Outcome::fail(FailureCode::Unauthenticated, "Authentication required."),
1449 };
1450 // An agent's token works through the API only: its sandbox has its
1451 // own way to push, to its own pull request.
1452 if a.viewer.as_ref().is_some_and(|user| user.kind == PrincipalKind::Agent) {
1453 return Ok(Outcome::fail(
1454 FailureCode::Forbidden,
1455 "A g1t agent's token cannot be used with git.",
1456 ));
1457 }
1458 if let (true, Some(user)) = (write, &a.viewer)
1459 && !user.verified
1460 {
1461 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
1462 }
1463 let repo = match found {
1464 Some(repo) => {
1465 let allowed = if write {
1466 can_write(&repo, &a.viewer)
1467 } else {
1468 self.may_read(&repo, &a.viewer).await?
1469 };
1470 if !allowed {
1471 return Ok(denied());
1472 }
1473 // An archived repository, or a pull request's copy of one,
1474 // is read-only.
1475 if write {
1476 let archived = match &repo.fork_of {
1477 Some(source) => self.registry.by_id(source).await?,
1478 None => Some(repo.clone()),
1479 };
1480 match archived {
1481 Some(source) => {
1482 if let Some((code, message)) = lifecycle::read_only_refusal(&source) {
1483 return Ok(Outcome::fail(code, format!("{message}\n")));
1484 }
1485 }
1486 // The repository it was copied from is deleted.
1487 None => return Ok(denied()),
1488 }
1489 }
1490 repo
1491 }
1492 None => {
1493 // Push to create, in a workspace the pusher belongs to.
1494 let owner = a
1495 .viewer
1496 .as_ref()
1497 .filter(|user| write && user.is_member(&a.path.namespace.to_lowercase()));
1498 let Some(owner) = owner else {
1499 return Ok(denied());
1500 };
1501 let created = self.create(push_to_create(owner, &a.path)).await?;
1502 match created {
1503 Outcome::Ok(repo) => repo,
1504 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
1505 }
1506 }
1507 };
1508 // A push, or a credential to push with, waits while the repository
1509 // moves between namespaces (moves.rs), and goes to where it is now.
1510 if write {
1511 return Ok(match self.unpaused(repo).await? {
1512 Ok(repo) => Outcome::Ok(repo),
1513 Err((code, message)) => Outcome::fail(code, format!("{message}\n")),
1514 });
1515 }
1516 Ok(Outcome::Ok(repo))
1517 }
1518
1519 async fn land(&self, a: LandArgs) -> Result<Outcome<Landed>> {
1520 let actor: Viewer = Some(a.actor.clone());
1521 let Some(source) = self.registry.by_id(&a.source_id).await? else {
1522 return Ok(not_found());
1523 };
1524 // A fork lands on the repository it came from; a branch on its own.
1525 let target = match &source.fork_of {
1526 Some(id) => self.registry.by_id(id).await?,
1527 None => Some(source.clone()),
1528 };
1529 let Some(target) = target.filter(|repo| can_read(repo, &actor)) else {
1530 return Ok(not_found());
1531 };
1532 if !registry::can(&target, &actor, Capability::Merge) {
1533 return Ok(Outcome::fail(
1534 FailureCode::Forbidden,
1535 access::needs(Capability::Merge, &format!("{}/{}", target.namespace, target.name)),
1536 ));
1537 }
1538 if !a.actor.verified {
1539 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
1540 }
1541 if let Some((code, message)) = lifecycle::read_only_refusal(&target) {
1542 return Ok(Outcome::fail(code, message));
1543 }
1544 // Moving between namespaces: wait for it (moves.rs). Both are read
1545 // again once it is done, for their new keys.
1546 let (source, target) = match (self.unpaused(source).await?, self.unpaused(target).await?) {
1547 (Ok(source), Ok(target)) => (source, target),
1548 (Err((code, message)), _) | (_, Err((code, message))) => return Ok(Outcome::fail(code, message)),
1549 };
1550
1551 let branch = &a.target_branch.clone().unwrap_or_else(|| target.default_branch.clone());
1552 let from_fork = source.id != target.id;
1553 let source_branch = match a.branch {
1554 Some(name) if !from_fork && name == *branch => {
1555 return Ok(Outcome::fail(
1556 FailureCode::Invalid,
1557 format!("{branch} cannot be merged into itself."),
1558 ));
1559 }
1560 Some(name) => name,
1561 None if from_fork => branch.clone(),
1562 None => {
1563 return Ok(Outcome::fail(
1564 FailureCode::Invalid,
1565 "Say which branch to merge.",
1566 ));
1567 }
1568 };
1569
1570 self.live(&source).await?;
1571 let source_git = self.store.open(&store_key(&source)).await?;
1572 let target_git = self.store.open(&store_key(&target)).await?;
1573 let history = source_git.log(&source_branch, MAX_ANCESTRY).await?;
1574 let Some(new) = history.first().map(|commit| commit.hash.clone()) else {
1575 return Ok(Outcome::fail(
1576 FailureCode::Conflict,
1577 "This pull request has no commits to merge.",
1578 ));
1579 };
1580 let old = target_git
1581 .log(branch, 1)
1582 .await?
1583 .into_iter()
1584 .next()
1585 .map(|commit| commit.hash);
1586
1587 if old.as_deref() == Some(new.as_str()) {
1588 return Ok(Outcome::Ok(Landed {
1589 commit: new,
1590 previous: None,
1591 }));
1592 }
1593 // Moving the branch to a commit that does not descend from its
1594 // current head would discard whatever landed in between.
1595 if let Some(old) = &old
1596 && !descends_from(&source_git, &history, old).await?
1597 {
1598 let remedy = if from_fork {
1599 format!("Pull {branch} into the pull request's fork, push, and merge again.")
1600 } else {
1601 format!("Merge {branch} into {source_branch}, push, and merge again.")
1602 };
1603 return Ok(Outcome::fail(
1604 FailureCode::Conflict,
1605 format!("{branch} has moved since this pull request was opened. {remedy}"),
1606 ));
1607 }
1608
1609 // For a branch the objects are already in the target; sending them
1610 // again is harmless and keeps one way of moving a ref.
1611 let source_access = source_git.access(Scope::Read).await?;
1612 let target_access = target_git.access(Scope::Write).await?;
1613 let pushed =
1614 land::fast_forward(&source_access, &target_access, branch, old.as_deref(), &new)
1615 .await?;
1616 self.refs_moved(&target.id).await;
1617 if let Err(reason) = pushed {
1618 // Most often another pull request landed between the check and the push.
1619 return Ok(Outcome::fail(
1620 FailureCode::Conflict,
1621 format!("{branch} could not be updated: {reason}"),
1622 ));
1623 }
1624 self.publish_push(
1625 &target,
1626 &format!("refs/heads/{branch}"),
1627 old.as_deref(),
1628 &new,
1629 Some(&a.actor),
1630 None,
1631 )
1632 .await?;
1633 Ok(Outcome::Ok(Landed {
1634 commit: new,
1635 previous: old,
1636 }))
1637 }
1638
1639 async fn compare(&self, a: CompareArgs) -> Result<Outcome<Comparison>> {
1640 let Some(repo) = self
1641 .visible(self.registry.by_id(&a.repo_id).await?, &a.viewer)
1642 .await?
1643 else {
1644 return Ok(not_found());
1645 };
1646 let git = self.read_git(&repo).await?;
1647 let head_ref = a.head.as_deref().unwrap_or(&repo.default_branch);
1648 // A pull request into another branch is compared from where it
1649 // left that branch.
1650 let base_branch = a.base_branch.clone();
1651 // The head's history is only searched when the base is worked out
1652 // from another branch.
1653 let depth = if a.base.is_some() || is_commit_hash(head_ref) { 1 } else { MAX_ANCESTRY };
1654 let history = git.log(head_ref, depth).await?;
1655 let Some(head) = history.first() else {
1656 return Ok(Outcome::fail(
1657 FailureCode::Conflict,
1658 "There are no commits to compare.",
1659 ));
1660 };
1661
1662 // Where the head's history meets the default branch of `against`,
1663 // or the branch asked for.
1664 let shared_with = async |against: &Repo| -> Result<Option<String>> {
1665 let against_git = self.read_git(against).await?;
1666 let branch = base_branch.as_deref().unwrap_or(&against.default_branch);
1667 let shared: HashSet<String> = against_git
1668 .log(branch, MAX_ANCESTRY)
1669 .await?
1670 .into_iter()
1671 .map(|commit| commit.hash)
1672 .collect();
1673 nearest_ancestor_in(&git, &history, &shared).await
1674 };
1675 let base = match (a.base, &repo.fork_of) {
1676 (Some(base), _) => Some(base),
1677 // A fork is compared with the last commit it shares with the
1678 // repository it came from.
1679 (None, Some(target_id)) => match self.registry.by_id(target_id).await? {
1680 Some(target) => shared_with(&target).await?,
1681 None => None,
1682 },
1683 // A branch, with the point where it left the default branch.
1684 // A single commit, with its first parent.
1685 (None, None) if is_commit_hash(head_ref) => head.parents.first().cloned(),
1686 (None, None) if head_ref != base_branch.as_deref().unwrap_or(&repo.default_branch) => {
1687 shared_with(&repo).await?
1688 }
1689 (None, None) => head.parents.first().cloned(),
1690 };
1691 let base_tree = match &base {
1692 Some(base) => git
1693 .log(base, 1)
1694 .await?
1695 .into_iter()
1696 .next()
1697 .map(|commit| commit.tree_hash),
1698 None => None,
1699 };
1700 let (files, truncated) =
1701 diff::compare_trees(&git, base_tree.as_deref(), &head.tree_hash).await?;
1702 Ok(Outcome::Ok(Comparison {
1703 base,
1704 head: head.hash.clone(),
1705 files,
1706 truncated,
1707 }))
1708 }
1709
1710 /// Reports that `git_ref` of `repo` (a full ref) now points to `after`,
1711 /// moved by `actor` (marked when that was a workflow job's token).
1712 /// `commits` is how many the move brought, when the caller knows: one
1713 /// for a file committed on the site or a branch brought up to date.
1714 async fn publish_push(
1715 &self,
1716 repo: &Repo,
1717 git_ref: &str,
1718 before: Option<&str>,
1719 after: &str,
1720 actor: Option<&User>,
1721 commits: Option<u32>,
1722 ) -> Result<()> {
1723 let caused_by_job = actor.and_then(g1t_contracts::events::job_run_of).map(str::to_owned);
1724 self.publish_git_push(repo, git_ref, before, after, actor.map(|user| user.id.clone()), false, caused_by_job, commits)
1725 .await
1726 }
1727
1728 /// `publish_push`, saying whether the push reached the store without
1729 /// being scanned for secrets first.
1730 #[allow(clippy::too_many_arguments)]
1731 async fn publish_git_push(
1732 &self,
1733 repo: &Repo,
1734 git_ref: &str,
1735 before: Option<&str>,
1736 after: &str,
1737 actor: Option<String>,
1738 unscanned: bool,
1739 caused_by_job: Option<String>,
1740 commits: Option<u32>,
1741 ) -> Result<()> {
1742 self.publish(NewEvent {
1743 kind: "git.push",
1744 source: SOURCE,
1745 repo_id: Some(repo.id.clone()),
1746 actor,
1747 data: GitPush {
1748 repo_id: repo.id.clone(),
1749 git_ref: git_ref.to_owned(),
1750 before: before.map(str::to_owned),
1751 after: after.to_owned(),
1752 default_branch: git_ref.strip_prefix("refs/heads/")
1753 == Some(repo.default_branch.as_str()),
1754 unscanned,
1755 caused_by_job,
1756 commits,
1757 },
1758 })
1759 .await
1760 }
1761
1762 /// Git over HTTPS. Only what decides the answer happens before it:
1763 /// the repository, who is asking and whether they may, the free
1764 /// workspace limits, push protection, and the store's own answer. The
1765 /// audit entry and what a push changed are recorded once git has its
1766 /// answer. Each answer says how long its steps took (`Server-Timing`).
1767 async fn git_http(&self, request: Request, env: &Env, ctx: &Context) -> Result<Response> {
1768 let mut timing = git_http::Timing::start();
1769 let Some(git) = git_http::parse(&request.url()?) else {
1770 return Response::error("Not found", 404);
1771 };
1772 let response = match self.answer_git(request, &git, env, ctx, &mut timing).await {
1773 Ok(response) => response,
1774 // The git store is busy: git hears when to try again.
1775 Err(error) => match resilience::busy(&error.to_string()) {
1776 Some(busy) => git_http::busy_response(busy)?,
1777 None => return Err(error),
1778 },
1779 };
1780 timing.apply(response)
1781 }
1782
1783 async fn answer_git(
1784 &self,
1785 request: Request,
1786 git: &git_http::GitRequest,
1787 env: &Env,
1788 ctx: &Context,
1789 timing: &mut git_http::Timing,
1790 ) -> Result<Response> {
1791 let write = git.service == GitService::ReceivePack;
1792 let get = request.method() == Method::Get;
1793 let identity = env.service("IDENTITY")?;
1794 // The repository and the caller's credentials, at once. A fetch may
1795 // go by the row as read a moment ago, for the same clone's next
1796 // request; a push always reads it. Anonymous callers cost nothing.
1797 let lookup = async {
1798 if write {
1799 self.registry.by_path(&git.path).await
1800 } else {
1801 self.registry.by_path_recent(&git.path).await
1802 }
1803 };
1804 let (found, viewer) =
1805 futures_util::future::join(lookup, git_http::viewer(&request, &identity)).await;
1806 let mut found = found?;
1807 timing.mark("repo");
1808 // A workspace alias staff set (identity's aliases.rs: `g1t` for
1809 // `flagon-io`) is answered in place, as the repository under the
1810 // workspace's slug: pushes and some clients do not follow
1811 // redirects. Everything after this sees only the workspace's slug.
1812 let aliased = match found {
1813 Some(_) => None,
1814 None => git_http::aliased(git, &identity).await?,
1815 };
1816 if let Some(aliased) = &aliased {
1817 found = if write {
1818 self.registry.by_path(&aliased.path).await?
1819 } else {
1820 self.registry.by_path_recent(&aliased.path).await?
1821 };
1822 timing.mark("alias");
1823 }
1824 let git = aliased.as_ref().unwrap_or(git);
1825 if found.is_none() {
1826 // A workspace that was renamed: git follows a redirect when it
1827 // first asks for refs, and uses the new address from then on.
1828 // A repository transferred to another workspace: the same, to
1829 // its new path. Fetches and pushes both follow either.
1830 let url = request.url()?;
1831 let (renamed, moved) = futures_util::future::join(
1832 git_http::renamed(&url, &identity),
1833 self.registry.resolve_moved(&git.path),
1834 )
1835 .await;
1836 timing.mark("moved");
1837 if let Some(location) = renamed? {
1838 return git_http::moved(&location, get);
1839 }
1840 if let Some(now) = moved?
1841 && let Some(location) = git_http::transferred(&url, &now)
1842 {
1843 return git_http::moved(&location, get);
1844 }
1845 }
1846 let viewer = viewer?;
1847 // A person who has not confirmed their email address can do
1848 // nothing with git until they do: told so, not asked to sign in.
1849 if viewer.as_ref().is_some_and(g1t_contracts::User::awaits_confirmation) {
1850 let site = request.url()?.origin().ascii_serialization();
1851 return git_http::refuse(Outcome::<()>::fail(
1852 FailureCode::Forbidden,
1853 g1t_contracts::accounts::confirm_email_first(&site),
1854 ));
1855 }
1856 // A run credential is checked against its grants, then acts as the
1857 // person it works for. See run_access.rs.
1858 let (request, viewer, audit) = match self.admit_git(request, git, viewer, found.as_ref()).await? {
1859 run_access::Admitted::Go { request, viewer, entry } => (request, viewer, entry),
1860 run_access::Admitted::Refused(response) => return Ok(response),
1861 };
1862 let mut after = AfterGit {
1863 audit,
1864 status: 0,
1865 message: None,
1866 push: None,
1867 };
1868 let repo = match self.authorize_git(&git.path, &viewer, git.service, found).await? {
1869 Outcome::Ok(repo) => repo,
1870 refused => {
1871 let response = git_http::refuse(refused)?;
1872 after.ended(response.status_code(), None);
1873 after.spawn(env, ctx);
1874 return Ok(response);
1875 }
1876 };
1877 // A pull request's working copy removed after it closed is made
1878 // again before git uses it (forks.rs).
1879 self.live(&repo).await?;
1880 timing.mark("access");
1881 // Clones check out the default branch g1t keeps, which can have
1882 // changed since the store made the repository.
1883 let default_branch = repo.fork_of.is_none().then(|| repo.default_branch.clone());
1884 let key = store_key(&repo);
1885 let scope = if write { Scope::Write } else { Scope::Read };
1886 let mut request = request;
1887 let protocol = refs_cache::protocol(request.headers().get("git-protocol")?.as_deref());
1888 // A fetch's POST is read here, to tell an `ls-refs` from a fetch of
1889 // objects; the store would have it read in full anyway.
1890 let body = if !write && !get { Some(request.bytes().await?) } else { None };
1891 // What it asks the store, for the meters (meters.rs).
1892 let call = git_ops::classify(git.service, git.endpoint, get, body.as_deref());
1893 // Answers kept from the usual store may name refs the fallback
1894 // store does not have (fallback.rs): none are used, or kept.
1895 let fallback = self.store.on_fallback(&key);
1896 // An answer that lists refs may have been kept: see refs_cache.rs.
1897 let kept_key = refs_cache::kind(git, get, protocol, body.as_deref())
1898 .filter(|_| !fallback)
1899 .zip(refs_cache::usable(registry::refs_state(&repo.id), now_ms()))
1900 .map(|(kind, version)| {
1901 refs_cache::Key::new(&repo.id, version, default_branch.as_deref(), protocol, &kind)
1902 });
1903 // A fresh clone's pack may have been kept too: see pack_cache.rs.
1904 // Under the same refs version, so never across a change to them.
1905 let pack_key = self
1906 .packs
1907 .as_ref()
1908 .filter(|_| !fallback)
1909 .and_then(|_| {
1910 let encoding = request.headers().get("content-encoding").ok().flatten();
1911 pack_cache::cacheable(git, get, protocol, encoding.as_deref(), body.as_deref())
1912 })
1913 .zip(refs_cache::usable(registry::refs_state(&repo.id), now_ms()))
1914 .map(|(normalized, version)| pack_cache::Key::new(&repo.id, version, &normalized));
1915 // A kept answer and the free workspace limits, with a kept
1916 // credential and, for a push, what the repository holds looked up
1917 // alongside. A kept answer goes back without waiting for the
1918 // credential, which it does not need.
1919 let pushing = write && !get;
1920 let ((answer, pack, (limited, held)), kept_access) = {
1921 let shared = self.shared.as_deref();
1922 let answer_and_limits = std::pin::pin!(futures_util::future::join3(
1923 async {
1924 match &kept_key {
1925 Some(kept_key) => refs_cache::get(shared, kept_key).await,
1926 None => None,
1927 }
1928 },
1929 async {
1930 match (&pack_key, self.packs.as_deref()) {
1931 (Some(pack_key), Some(packs)) => pack_cache::get(packs, pack_key).await,
1932 _ => None,
1933 }
1934 },
1935 futures_util::future::join(self.git_limits(call, git, &repo, env), async {
1936 if pushing { Some(self.held(&repo).await) } else { None }
1937 }),
1938 ));
1939 let kept_access = std::pin::pin!(self.store.kept_access(&key, scope));
1940 match futures_util::future::select(answer_and_limits, kept_access).await {
1941 futures_util::future::Either::Left((first, kept_access)) => {
1942 let answered = first.0.is_some() || first.1.is_some() || matches!(first.2.0, Ok(Some(_)) | Err(_));
1943 (first, if answered { None } else { kept_access.await })
1944 }
1945 futures_util::future::Either::Right((kept_access, first)) => (first.await, kept_access),
1946 }
1947 };
1948 timing.mark("kept");
1949 // A kept pack first: it never reaches the store, so it is never an
1950 // operation, and a free workspace past its operation cap still gets
1951 // it. (The other limits are a push's, and a pack is only a fetch.)
1952 if let Some(kept) = pack {
1953 timing.note("pack", "hit");
1954 let sent = body.as_ref().map_or(0, |body| body.len() as u64);
1955 meters::record(pack_cache::HIT, &key, sent, kept.size);
1956 after.ended(200, None);
1957 after.spawn(env, ctx);
1958 return kept.response();
1959 }
1960 if let Some((response, status, message)) = limited? {
1961 after.ended(status, Some(message.to_owned()));
1962 after.spawn(env, ctx);
1963 return Ok(response);
1964 }
1965 if pack_key.is_some() {
1966 timing.note("pack", "miss");
1967 }
1968 if let (Some((entry, found)), Some(kept_key)) = (answer, &kept_key) {
1969 timing.note("refs", found.as_str());
1970 if found == refs_cache::Found::Shared {
1971 let (kept_key, entry) = (kept_key.clone(), entry.clone());
1972 ctx.wait_until(async move { refs_cache::keep_in_colo(&kept_key, &entry).await });
1973 }
1974 // Never reached the store: never an operation.
1975 meters::record(call.cached_meter(), &key, 0, entry.body.len() as u64);
1976 after.ended(200, None);
1977 after.spawn(env, ctx);
1978 return entry.response();
1979 }
1980 if kept_key.is_some() {
1981 timing.note("refs", "miss");
1982 }
1983 // An anonymous fetch the store is about to answer: an operation for
1984 // the repository's workspace, so limited per repository (limits.rs).
1985 if limits::counts_as_anonymous_fetch(call, viewer.is_none())
1986 && g1t_kit::limits::check(env, limits::ANONYMOUS_FETCH, repo.id.clone()).await.limited()
1987 {
1988 let response = limits::too_many_anonymous_fetches(&format!("{}/{}", repo.namespace, repo.name))?;
1989 after.ended(429, Some("Too many anonymous fetches.".to_owned()));
1990 after.spawn(env, ctx);
1991 return Ok(response);
1992 }
1993 // The store's credential: one made a moment ago, here or in another
1994 // isolate (see store.rs), or a new one.
1995 let access = match kept_access {
1996 Some((access, from)) => {
1997 timing.note("cred", from.as_str());
1998 access
1999 }
2000 None => {
2001 let access = self.store.mint_access(&key, scope).await?;
2002 timing.mark("mint");
2003 timing.note("cred", "mint");
2004 access
2005 }
2006 };
2007 // Should the store turn a kept credential down, a fetch's first
2008 // request is tried again with a new one; the requests after it then
2009 // have that one too.
2010 let again = if get { Some(request.clone()?) } else { None };
2011 // A push's checks: workflow files, the rules of the branches and
2012 // tags it changes, saying which rule and why (rules.rs), and push
2013 // protection, which refuses a push that adds a secret
2014 // (secret_scan.rs). See push_checks.rs.
2015 let checks = async |head: &[u8], whole: bool, scan: bool| self.check_receive(&repo, viewer.as_ref(), head, whole, scan).await;
2016 // What a push may bring (pack_limits.rs): the repository's size is
2017 // its own and its pull requests' working copies'.
2018 let limits = if let Some(held) = held {
2019 git_http::PushLimits {
2020 held,
2021 repo_limit: self.repo_limit,
2022 large: self.large_pushes,
2023 ..git_http::PushLimits::default()
2024 }
2025 } else {
2026 git_http::PushLimits::default()
2027 };
2028 let mut outcome = git_http::forward(
2029 request,
2030 body,
2031 git,
2032 &access,
2033 checks,
2034 default_branch.as_deref(),
2035 limits,
2036 timing,
2037 )
2038 .await?;
2039 let turned_down = matches!(
2040 &outcome,
2041 git_http::Push::Forwarded(forwarded) if matches!(forwarded.response.status_code(), 401 | 403)
2042 );
2043 if turned_down {
2044 self.store.forget_access(&key).await;
2045 if let Some(again) = again {
2046 let access = self.store.mint_access(&key, scope).await?;
2047 outcome = git_http::forward(
2048 again,
2049 None,
2050 git,
2051 &access,
2052 async |_: &[u8], _: bool, _: bool| Ok(push_checks::Checks::clear()),
2053 default_branch.as_deref(),
2054 git_http::PushLimits::default(),
2055 timing,
2056 )
2057 .await?;
2058 }
2059 }
2060 let forwarded =
2061 match outcome {
2062 git_http::Push::Forwarded(forwarded) => forwarded,
2063 git_http::Push::Refused(response) => {
2064 after.ended(403, Some("The push was declined by rules.".to_owned()));
2065 after.spawn(env, ctx);
2066 return Ok(response);
2067 }
2068 git_http::Push::Blocked(response) => {
2069 after.ended(403, Some("The push adds a secret.".to_owned()));
2070 after.spawn(env, ctx);
2071 return Ok(response);
2072 }
2073 git_http::Push::Declined(response, reason) => {
2074 after.ended(403, Some(format!("The push was declined: {reason}.")));
2075 after.spawn(env, ctx);
2076 return Ok(response);
2077 }
2078 };
2079 if forwarded.from_store {
2080 let received = forwarded
2081 .response
2082 .headers()
2083 .get("content-length")?
2084 .and_then(|length| length.parse().ok())
2085 .unwrap_or(0);
2086 meters::record(call.meter(), &key, forwarded.sent, received);
2087 }
2088 timing.mark("store");
2089 let mut response = forwarded.response;
2090 let status = response.status_code();
2091 if write && !get {
2092 // A push: the store has moved its refs once it has answered in
2093 // full, so the answer is read before the change is recorded, and
2094 // only then goes back. Whoever fetches after it sees the push.
2095 let headers = response.headers().clone();
2096 headers.delete("content-length")?;
2097 let report = response.bytes().await?;
2098 self.refs_moved(&repo.id).await;
2099 timing.mark("refs");
2100 response = Response::from_bytes(report)?.with_headers(headers).with_status(status);
2101 } else if let (Some(kept_key), 200) = (&kept_key, status) {
2102 // A miss: this answer is kept for the next to ask.
2103 let headers = response.headers().clone();
2104 headers.delete("content-length")?;
2105 let body = response.bytes().await?;
2106 if let Some(content_type) = headers.get("content-type")? {
2107 let entry = refs_cache::Entry { content_type, body: body.clone() };
2108 if entry.keepable() {
2109 let shared = self.shared.clone();
2110 let kept_key = kept_key.clone();
2111 ctx.wait_until(async move { refs_cache::keep(shared.as_deref(), &kept_key, &entry).await });
2112 }
2113 }
2114 response = Response::from_bytes(body)?.with_headers(headers).with_status(status);
2115 } else if let (Some(pack_key), Some(packs), true) = (&pack_key, &self.packs, forwarded.from_store) {
2116 // A fresh clone the bucket did not have: counted, and its pack
2117 // kept as it streams to git, when it is a whole one.
2118 meters::record(pack_cache::MISS, &key, forwarded.sent, 0);
2119 // Past the repository's limit on writes to the bucket, the pack
2120 // goes to git without being kept (limits.rs).
2121 if status == 200 && !g1t_kit::limits::check(env, limits::PACK_FILL, repo.id.clone()).await.limited() {
2122 let store_key = key.clone();
2123 let measured = Box::new(move |bytes: u64| meters::record_bytes(pack_cache::MISS, &store_key, 0, bytes));
2124 let (teed, filling) = pack_cache::tee(response, packs.clone(), pack_key, measured)?;
2125 response = teed;
2126 if let Some(filling) = filling {
2127 let pack_key = pack_key.clone();
2128 ctx.wait_until(async move {
2129 let filled = filling.await;
2130 if !matches!(filled, pack_cache::Filled::Kept { .. } | pack_cache::Filled::Abandoned) {
2131 worker::console_warn!("pack {} not kept: {filled:?}", pack_key.as_str());
2132 }
2133 });
2134 }
2135 }
2136 }
2137 after.ended(status, None);
2138 if status == 200 && (forwarded.pack_bytes > 0 || !forwarded.pushed.is_empty() || !forwarded.deleted.is_empty()) {
2139 after.push = Some(PushDone {
2140 repo,
2141 pushed: forwarded.pushed,
2142 deleted: forwarded.deleted,
2143 pack_bytes: forwarded.pack_bytes,
2144 caused_by_job: viewer.as_ref().and_then(g1t_contracts::events::job_run_of).map(str::to_owned),
2145 // The calendar credits people, never a job's, an agent's or a workspace's token.
2146 credit: viewer
2147 .as_ref()
2148 .filter(|user| user.kind == PrincipalKind::User && g1t_contracts::events::job_run_of(user).is_none())
2149 .map(|user| user.id.clone()),
2150 actor: viewer.map(|user: User| user.id),
2151 unscanned: forwarded.unscanned,
2152 });
2153 }
2154 after.spawn(env, ctx);
2155 Ok(response)
2156 }
2157
2158 /// The answer for a request a free workspace's limits stop, or a push
2159 /// to a full repository, with its status and reason for the audit log;
2160 /// `None` to go on.
2161 ///
2162 /// A clone, fetch or push is a git operation, which the git store
2163 /// charges g1t for: counted for billing once the answer has gone back
2164 /// (meters.rs), and a free workspace far past its share is slowed down
2165 /// rather than charged (see git_ops.rs). Whether it is past it is
2166 /// decided from counts this isolate already holds: the database is not
2167 /// asked on the way. A free workspace is never charged for private
2168 /// storage: once its private repositories hold the free amount, pushes
2169 /// to them stop, checked when a push begins so that git shows the
2170 /// reason. So do pushes to a repository at the store's size limit.
2171 async fn git_limits(
2172 &self,
2173 call: git_ops::GitCall,
2174 git: &git_http::GitRequest,
2175 repo: &Repo,
2176 env: &Env,
2177 ) -> Result<Option<(Response, u16, &'static str)>> {
2178 let namespace = git.path.namespace.to_lowercase();
2179 if meters::mapping_now().billable(call.meter()) > 0.0 {
2180 let now = now_ms();
2181 let hour = git_ops::hour_key(&rfc3339(now));
2182 let limits = git_ops::Limits::from_env(env);
2183 if let Some((month, hour_ops)) = git_ops::standing(&namespace, &hour, now)
2184 && git_ops::slow_down(month + 1, hour_ops + 1, limits.free_cap, limits.hourly)
2185 && git_ops::is_free_kept(env.service("BILLING").ok().as_ref(), &namespace).await
2186 {
2187 return Ok(Some((
2188 git_ops::too_many(&namespace, limits.free_cap, limits.hourly)?,
2189 429,
2190 "Too many git operations this hour.",
2191 )));
2192 }
2193 }
2194 if git.service == GitService::ReceivePack && git.endpoint == "info/refs" {
2195 let held = self.held(repo).await;
2196 if held >= self.repo_limit {
2197 let message = format!(
2198 "{}/{} 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",
2199 repo.namespace,
2200 repo.name,
2201 pack_limits::megabytes(held)
2202 );
2203 return Ok(Some((Response::error(message, 403)?, 403, "The repository is full.")));
2204 }
2205 }
2206 if git.service == GitService::ReceivePack && git.endpoint == "info/refs" && repo.is_private {
2207 let free = git_ops::free_private_bytes(env);
2208 let held = self.registry.private_bytes(&namespace).await.unwrap_or(0);
2209 if git_ops::storage_full(held, free)
2210 && git_ops::is_free(env.service("BILLING").ok().as_ref(), &namespace).await
2211 {
2212 return Ok(Some((
2213 git_ops::storage_full_response(&namespace, held, free)?,
2214 403,
2215 "Free private storage is full.",
2216 )));
2217 }
2218 }
2219 if git.service == GitService::ReceivePack && git.endpoint == "info/refs" && !repo.is_private {
2220 let free = git_ops::free_public_bytes(env);
2221 if free > 0 {
2222 let held = self.registry.public_bytes(&namespace).await.unwrap_or(0);
2223 if git_ops::public_storage_full(held, free)
2224 && git_ops::is_free(env.service("BILLING").ok().as_ref(), &namespace).await
2225 {
2226 return Ok(Some((
2227 git_ops::public_storage_full_response(&namespace, held, free)?,
2228 403,
2229 "Free public storage is full.",
2230 )));
2231 }
2232 }
2233 }
2234 Ok(None)
2235 }
2236
2237 /// What a repository and its pull requests' working copies hold, as
2238 /// g1t counts it: read for a push's first request, kept a minute for
2239 /// the rest of it.
2240 async fn held(&self, repo: &Repo) -> u64 {
2241 let root = repo.fork_of.clone().unwrap_or_else(|| repo.id.clone());
2242 let now = now_ms();
2243 if let Some(held) = HELD.with(|held| held.borrow().get(&root, now)) {
2244 return held;
2245 }
2246 let held = self.registry.stored_bytes(&root).await.unwrap_or(0).max(0) as u64;
2247 HELD.with(|kept| kept.borrow_mut().put(root, held, now));
2248 held
2249 }
2250
2251 /// A `git.deleted` event for each branch or tag a push deleted that is
2252 /// gone from the store: the store can refuse one ref and accept
2253 /// another. As many as a push announces of tags and branches it moves.
2254 async fn publish_deleted(
2255 &self,
2256 repo: &Repo,
2257 deleted: &[(String, String)],
2258 actor: Option<&str>,
2259 caused_by_job: Option<&str>,
2260 ) -> Result<()> {
2261 let stored = self.store.open(&store_key(repo)).await?;
2262 let is_branch = |git_ref: &str| git_ref.starts_with("refs/heads/");
2263 let tags = deleted.iter().filter(|(git_ref, _)| !is_branch(git_ref)).count();
2264 let announced = deleted
2265 .iter()
2266 .filter(|(git_ref, _)| is_branch(git_ref))
2267 .take(MAX_PUSH_BRANCH_EVENTS)
2268 .chain(deleted.iter().filter(|(git_ref, _)| !is_branch(git_ref) && tags <= MAX_PUSH_TAG_EVENTS));
2269 for (git_ref, before) in announced {
2270 let named = git_ref.strip_prefix("refs/heads/").unwrap_or(git_ref);
2271 let gone = match stored.log(named, 1).await {
2272 Ok(head) => head.is_empty(),
2273 Err(error) => {
2274 worker::console_error!("whether {git_ref} of {} was deleted is not known: {error}", repo.name);
2275 false
2276 }
2277 };
2278 if !gone {
2279 continue;
2280 }
2281 self.publish(NewEvent {
2282 kind: "git.deleted",
2283 source: SOURCE,
2284 repo_id: Some(repo.id.clone()),
2285 actor: actor.map(str::to_owned),
2286 data: GitDeleted {
2287 repo_id: repo.id.clone(),
2288 git_ref: git_ref.clone(),
2289 before: before.clone(),
2290 caused_by_job: caused_by_job.map(str::to_owned),
2291 },
2292 })
2293 .await?;
2294 }
2295 Ok(())
2296 }
2297
2298 /// What a push changed, recorded once git has its answer.
2299 async fn record_push(&self, push: PushDone) -> Result<()> {
2300 let PushDone {
2301 repo,
2302 pushed,
2303 deleted,
2304 pack_bytes,
2305 actor,
2306 unscanned,
2307 caused_by_job,
2308 credit,
2309 } = push;
2310 // What the push stored, for billing's storage meter. A failure only
2311 // leaves the count short.
2312 if pack_bytes > 0
2313 && let Err(error) = self.registry.add_stored_bytes(&repo, pack_bytes).await
2314 {
2315 worker::console_error!("stored bytes for {} not counted: {error}", repo.name);
2316 }
2317 if !deleted.is_empty() {
2318 self.publish_deleted(&repo, &deleted, actor.as_deref(), caused_by_job.as_deref()).await?;
2319 }
2320 if pushed.is_empty() {
2321 return Ok(());
2322 }
2323 // Artifacts' own push notifications are per repository, which does
2324 // not fit a repo per pull request, so the front end reports pushes
2325 // itself: one event for each branch that moved.
2326 let stored = self.store.open(&store_key(&repo)).await?;
2327 let announced = announced_refs(&pushed, &repo.default_branch);
2328 if announced.len() < pushed.len() {
2329 worker::console_log!(
2330 "push to {}: {} of {} refs announced",
2331 repo.name,
2332 announced.len(),
2333 pushed.len()
2334 );
2335 }
2336 for pushed in announced {
2337 // The store can refuse one ref and accept another, so each
2338 // branch is checked against where it actually is. A tag the
2339 // store cannot read back is taken as pushed.
2340 let moved = match pushed.branch() {
2341 Some(branch) => stored
2342 .log(branch, 1)
2343 .await?
2344 .first()
2345 .is_some_and(|commit| commit.hash == pushed.after),
2346 None => stored.log(&pushed.git_ref, 1).await.map_or(true, |head| {
2347 head.first().is_none_or(|commit| commit.hash == pushed.after)
2348 }),
2349 };
2350 // How many commits a branch push brought (push_commits.rs), for
2351 // the event and the person's contribution calendar. A log the
2352 // store cannot read leaves the event without a count and the
2353 // day short.
2354 let commits = match (moved, pushed.branch()) {
2355 (true, Some(_)) => match stored.log(&pushed.after, push_commits::MOST_PER_PUSH + 1).await {
2356 Ok(log) => Some(push_commits::new_commits(&log, pushed.before.as_deref())),
2357 Err(error) => {
2358 worker::console_error!("commits pushed to {} not counted: {error}", repo.name);
2359 None
2360 }
2361 },
2362 _ => None,
2363 };
2364 if let (Some(commits), Some(user_id), Some(branch)) = (commits, credit.as_deref(), pushed.branch())
2365 && push_commits::counts(branch, &repo.default_branch)
2366 && let Err(error) =
2367 push_commits::record(&self.registry.db, user_id, &repo.id, &rfc3339(now_ms())[..10], commits).await
2368 {
2369 worker::console_error!("commits pushed to {} not credited: {error}", repo.name);
2370 }
2371 if moved {
2372 self.publish_git_push(
2373 &repo,
2374 &pushed.git_ref,
2375 pushed.before.as_deref(),
2376 &pushed.after,
2377 actor.clone(),
2378 unscanned,
2379 caused_by_job.clone(),
2380 commits,
2381 )
2382 .await?;
2383 }
2384 }
2385 Ok(())
2386 }
2387}
2388
2389/// The most tags one push announces: a push of more (`git push --tags`
2390/// into a new repository, say) announces none of them, so it starts no
2391/// workflows or package reads, one per tag.
2392const MAX_PUSH_TAG_EVENTS: usize = 3;
2393/// The most branches one push announces. A push of more announces only
2394/// the default branch, if it moved, which the rest of g1t reads from.
2395const MAX_PUSH_BRANCH_EVENTS: usize = 1000;
2396
2397/// The refs of a push that are announced with a `git.push` event each.
2398/// Every ref is stored whatever this says; only the events are capped.
2399fn announced_refs<'a>(pushed: &'a [git_http::Pushed], default_branch: &str) -> Vec<&'a git_http::Pushed> {
2400 let (branches, tags): (Vec<&git_http::Pushed>, Vec<&git_http::Pushed>) =
2401 pushed.iter().partition(|pushed| pushed.branch().is_some());
2402 let mut announced = if branches.len() > MAX_PUSH_BRANCH_EVENTS {
2403 branches.into_iter().filter(|pushed| pushed.branch() == Some(default_branch)).collect()
2404 } else {
2405 branches
2406 };
2407 if tags.len() <= MAX_PUSH_TAG_EVENTS {
2408 announced.extend(tags);
2409 }
2410 announced
2411}
2412
2413/// A push the store accepted, to be recorded once git has its answer.
2414struct PushDone {
2415 repo: Repo,
2416 pushed: Vec<git_http::Pushed>,
2417 /// The branches and tags it asked to delete, each with the commit it
2418 /// pointed to.
2419 deleted: Vec<(String, String)>,
2420 pack_bytes: u64,
2421 actor: Option<String>,
2422 /// Too large to scan for secrets before it was stored.
2423 unscanned: bool,
2424 /// The run whose job's token pushed, if one did: its push starts no
2425 /// workflows.
2426 caused_by_job: Option<String>,
2427 /// The person credited with the push's commits on their contribution
2428 /// calendar: whoever pushed, when that is a person (push_commits.rs).
2429 credit: Option<String>,
2430}
2431
2432/// What a git request leaves for after its answer: its audit entry, with
2433/// how the request ended, and what a push changed.
2434struct AfterGit {
2435 audit: Option<Box<g1t_contracts::audit::NewAuditEntry>>,
2436 status: u16,
2437 message: Option<String>,
2438 push: Option<PushDone>,
2439}
2440
2441impl AfterGit {
2442 fn ended(&mut self, status: u16, message: Option<String>) {
2443 self.status = status;
2444 self.message = message;
2445 }
2446
2447 /// Does the work once the response is on its way. A failure is logged:
2448 /// git has already been told how its request went.
2449 fn spawn(self, env: &Env, ctx: &Context) {
2450 if self.audit.is_none() && self.push.is_none() {
2451 return;
2452 }
2453 let env = env.clone();
2454 ctx.wait_until(async move {
2455 let repos = match service(&env) {
2456 Ok(repos) => repos,
2457 Err(error) => {
2458 worker::console_error!("git request not recorded: {error}");
2459 return;
2460 }
2461 };
2462 repos.finish_git(self.audit, self.status, self.message).await;
2463 if let Some(push) = self.push
2464 && let Err(error) = repos.record_push(push).await
2465 {
2466 worker::console_error!("push not recorded: {error}");
2467 }
2468 repos.deferred.settle().await;
2469 });
2470 }
2471}
2472
2473fn service(env: &Env) -> Result<Repos<ArtifactsStore>> {
2474 let shared = shared::Shared::from_env(env).map(Rc::new);
2475 let deferred = Rc::new(store::Deferred::default());
2476 Ok(Repos {
2477 registry: Registry { db: env.d1("DB")? },
2478 store: ArtifactsStore::new(env, shared.clone(), deferred.clone())?,
2479 deferred,
2480 shared,
2481 packs: pack_cache::Packs::from_env(env).map(Rc::new),
2482 events: env.service("EVENTS")?,
2483 security: env.service("SECURITY").ok(),
2484 billing: env.service("BILLING").ok(),
2485 identity: env.service("IDENTITY").ok(),
2486 work: env.service("WORK").ok(),
2487 free_private_bytes: git_ops::free_private_bytes(env),
2488 fork_days: forks::retention_days(env),
2489 repo_limit: env
2490 .var("REPO_STORAGE_LIMIT_BYTES")
2491 .ok()
2492 .and_then(|value| value.to_string().parse().ok())
2493 .unwrap_or(pack_limits::DEFAULT_REPO_LIMIT_BYTES),
2494 large_pushes: git_http::LargePushes::from_var(env.var("LARGE_PUSHES").ok().map(|value| value.to_string()).as_deref()),
2495 placement: shards::Placement::from_vars(
2496 env.var("GITSTORE_NEW_REPOS").ok().map(|value| value.to_string()).as_deref(),
2497 env.var("GITSTORE_EU_NAMESPACE").ok().map(|value| value.to_string()).as_deref(),
2498 ),
2499 limits: shards::limits(env.var("GITSTORE_NAMESPACE_LIMITS").ok().map(|value| value.to_string()).as_deref()),
2500 })
2501}
2502
2503/// Writes what this isolate metered once the answer has gone back, every
2504/// few seconds at most: now, or once it is due, waiting in this request's
2505/// `wait_until` so nothing counted is left for a request that may never
2506/// come (meters.rs).
2507fn flush_later(env: &Env, ctx: &Context) {
2508 let Some(wait) = meters::plan_flush() else {
2509 return;
2510 };
2511 if let Ok(db) = env.d1("DB") {
2512 ctx.wait_until(async move { meters::flush_after(&db, wait).await });
2513 }
2514}
2515
2516/// `/backups/<job id>/parts/<number>`: the job and the part's number.
2517fn backup_part_path(path: &str) -> Option<(String, u16)> {
2518 let rest = path.strip_prefix("/backups/")?;
2519 let (job, number) = rest.split_once("/parts/")?;
2520 let number = number.parse::<u16>().ok()?;
2521 (!job.is_empty() && !job.contains('/')).then(|| (job.to_owned(), number))
2522}
2523
2524fn backups_off<T>() -> Outcome<T> {
2525 Outcome::fail(FailureCode::Conflict, "Backups are off on this installation: it has no storage for them.")
2526}
2527
2528/// One part of a backup's bundle, with the job's token in its header.
2529async fn backup_part(request: &mut Request, env: &Env, repos: &Repos<ArtifactsStore>, job_id: String, number: u16) -> Result<Response> {
2530 let Some(blobs) = backups::storage(env) else {
2531 return reply(&backups_off::<()>());
2532 };
2533 let token = request.headers().get(g1t_contracts::backups::TOKEN_HEADER)?.unwrap_or_default();
2534 let bytes = request.bytes().await?;
2535 let job = g1t_contracts::backups::BackupJobArgs { job_id, token };
2536 reply(&backups::part(&repos.registry.db, &blobs, &job, number, bytes).await?)
2537}
2538
2539#[cfg(test)]
2540mod backup_path_tests {
2541 use super::backup_part_path;
2542
2543 #[test]
2544 fn a_part_is_named_by_its_job_and_number() {
2545 assert_eq!(backup_part_path("/backups/bkp_1/parts/3"), Some(("bkp_1".to_owned(), 3)));
2546 assert_eq!(backup_part_path("/backups/bkp_1/parts/x"), None);
2547 assert_eq!(backup_part_path("/backups//parts/1"), None);
2548 assert_eq!(backup_part_path("/acme/rocket.git/info/refs"), None);
2549 }
2550}
2551
2552/// Read methods whose answer is an `Outcome`: when the git store is busy,
2553/// the site is told so in words instead of failing the page.
2554const OUTCOME_READS: [&str; 7] = ["tree", "blob", "log", "branches", "blame", "compare", "branch_drift"];
2555
2556#[event(fetch)]
2557async fn fetch(mut request: Request, env: Env, ctx: Context) -> Result<Response> {
2558 let mut repos = service(&env)?;
2559 // A part of a backup's bundle, as the API passes it on from the
2560 // sandbox: bytes, not JSON (backups.rs).
2561 if request.method() == Method::Put
2562 && let Some((job_id, number)) = backup_part_path(&request.path())
2563 {
2564 let answered = backup_part(&mut request, &env, &repos, job_id, number).await;
2565 repos.deferred.hand_over(&ctx);
2566 flush_later(&env, &ctx);
2567 return answered;
2568 }
2569 let Some(method) = rpc_method(&request) else {
2570 let answered = repos.git_http(request, &env, &ctx).await;
2571 repos.deferred.hand_over(&ctx);
2572 flush_later(&env, &ctx);
2573 return answered;
2574 };
2575 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
2576 // Git over HTTPS above always reads the primary.
2577 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
2578 repos.registry.db = db;
2579 let body: serde_json::Value = request.json().await?;
2580
2581 let answered = async { match method.as_str() {
2582 "get" => reply(&repos.get(args(body)?).await?),
2583 "get_by_id" => reply(&repos.get_by_id(args(body)?).await?),
2584 "readable" => {
2585 let a: ReadableArgs = args(body)?;
2586 reply(&repos.registry.readable(&a.ids, &a.viewer).await?)
2587 }
2588 // Work's contribution calendar: the commits a person pushed each
2589 // day, in repositories the viewer may read (push_commits.rs).
2590 "commit_days" => {
2591 let a: g1t_contracts::repos::CommitDaysArgs = args(body)?;
2592 reply(&push_commits::days(&repos.registry, &a.user_id, &a.since, &a.viewer).await?)
2593 }
2594 "public_namespaces" => {
2595 let a: PublicNamespacesArgs = args(body)?;
2596 reply(&repos.registry.public_namespaces(&a.owner_id).await?)
2597 }
2598 "path_by_id" => {
2599 let a: PathByIdArgs = args(body)?;
2600 reply(
2601 &repos
2602 .registry
2603 .by_id(&a.id)
2604 .await?
2605 .filter(|repo| repo.fork_of.is_none())
2606 .map(|repo| RepoPath {
2607 namespace: repo.namespace,
2608 name: repo.name,
2609 }),
2610 )
2611 }
2612 "list" => {
2613 let a: ListArgs = args(body)?;
2614 reply(
2615 &repos
2616 .registry
2617 .list(
2618 &a.viewer,
2619 a.query.as_deref(),
2620 a.namespace.as_deref(),
2621 a.member_only,
2622 )
2623 .await?,
2624 )
2625 }
2626 "create" => reply(&repos.create(args(body)?).await?),
2627 "transfer" => reply(&repos.transfer(args(body)?).await?),
2628 // A repository's lifecycle: see lifecycle.rs.
2629 "delete" => reply(&repos.delete(args(body)?).await?),
2630 "deleted" => reply(&repos.deleted(args(body)?).await?),
2631 "restore" => reply(&repos.restore(args(body)?).await?),
2632 "purge" => reply(&repos.purge(args(body)?).await?),
2633 "purge_due" => reply(&repos.purge_due(args(body)?).await?),
2634 "rename" => reply(&repos.rename(args(body)?).await?),
2635 "archive" => reply(&repos.archive(args(body)?).await?),
2636 "set_visibility" => reply(&repos.set_visibility(args(body)?).await?),
2637 "set_default_branch" => reply(&repos.set_default_branch(args(body)?).await?),
2638 "rename_branch" => reply(&repos.rename_branch(args(body)?).await?),
2639 "create_branch" => reply(&repos.create_branch(args(body)?).await?),
2640 "remove_branch" => reply(&repos.remove_branch(args(body)?).await?),
2641 "resolve_branch" => reply(&repos.resolve_branch(args(body)?).await?),
2642 "status_by_id" => reply(&repos.status_by_id(args(body)?).await?),
2643 "resolve_path" => {
2644 let a: ResolvePathArgs = args(body)?;
2645 reply(&repos.registry.resolve_moved(&a.path).await?)
2646 }
2647 "namespace_count" => {
2648 let a: NamespaceCountArgs = args(body)?;
2649 reply(&repos.registry.count_in(&a.namespace).await?)
2650 }
2651 "update" => reply(&repos.update(args(body)?).await?),
2652 "tree" => reply(&repos.tree(args(body)?).await?),
2653 "blob" => reply(&repos.blob(args(body)?).await?),
2654 "log" => reply(&repos.log(args(body)?).await?),
2655 "blame" => reply(&repos.blame(args(body)?).await?),
2656 "fork_for_pull" => reply(&repos.fork_for_pull(args(body)?).await?),
2657 "git_access" => reply(&repos.git_access(args(body)?).await?),
2658 "branches" => reply(&repos.branches(args(body)?).await?),
2659 "last_commits" => reply(&repos.last_commits(args(body)?).await?),
2660 "branch_drift" => reply(&repos.branch_drift(args(body)?).await?),
2661 "tags" => reply(&repos.tags(args(body)?).await?),
2662 // The About: what the Files page shows beside the files (about.rs).
2663 // What is kept behind the head is worked out again after the answer.
2664 "about" => {
2665 let (answer, refresh) = repos.about(args(body)?).await?;
2666 about::refresh_later(&env, &ctx, refresh);
2667 reply(&answer)
2668 }
2669 "languages" => {
2670 let (answer, refresh) = repos.languages(args(body)?).await?;
2671 about::refresh_later(&env, &ctx, refresh);
2672 reply(&answer)
2673 }
2674 "contributors" => {
2675 let (answer, refresh) = repos.contributors(args(body)?).await?;
2676 about::refresh_later(&env, &ctx, refresh);
2677 reply(&answer)
2678 }
2679 "license" => {
2680 let (answer, refresh) = repos.license(args(body)?).await?;
2681 about::refresh_later(&env, &ctx, refresh);
2682 reply(&answer)
2683 }
2684 "stars" => reply(&repos.stars(args(body)?).await?),
2685 "star" => reply(&repos.star(args(body)?).await?),
2686 "stargazers" => reply(&repos.stargazers(args(body)?).await?),
2687 "starred" => reply(&repos.starred(args(body)?).await?),
2688 "releases" => reply(&repos.releases(args(body)?).await?),
2689 "release" => reply(&repos.release(args(body)?).await?),
2690 "create_release" => reply(&repos.create_release(args(body)?).await?),
2691 "update_release" => reply(&repos.update_release(args(body)?).await?),
2692 "delete_release" => reply(&repos.delete_release(args(body)?).await?),
2693 "head" => reply(&repos.head(args(body)?).await?),
2694 "behind" => reply(&repos.behind(args(body)?).await?),
2695 "divergence" => reply(&repos.divergence(args(body)?).await?),
2696 "land" => reply(&repos.land(args(body)?).await?),
2697 "update_pull_branch" => reply(&repos.update_pull_branch(args(body)?).await?),
2698 // Services only: a pull request's test merge commit (test_merge.rs).
2699 "pull_merge_ref" => reply(&repos.pull_merge_ref(args(body)?).await?),
2700 "delete_branch" => reply(&repos.delete_branch(args(body)?).await?),
2701 "commit_file" => reply(&repos.commit_file(args(body)?).await?),
2702 "compare" => reply(&repos.compare(args(body)?).await?),
2703 // Services only: a pull request's commits, as rules look at them (rules.rs).
2704 "inspect_commits" => reply(&repos.inspect_commits(args(body)?).await?),
2705 "scan_history" => reply(&repos.scan_history(args(body)?).await?),
2706 "find_lockfiles" => reply(&repos.find_lockfiles(args(body)?).await?),
2707 "match_pattern" => reply(&repos.match_pattern(args(body)?).await?),
2708 "check_secret" => reply(&repos.check_secret(args(body)?).await?),
2709 "list_files" => reply(&repos.list_files(args(body)?).await?),
2710 // The API's tarball and zipball links, and the site opening them (archive_links.rs).
2711 "archive_link" => {
2712 let sealer = env.secret("REPOS_KEY").ok().and_then(|key| g1t_secrets::Sealer::new(&key.to_string()));
2713 reply(&repos.archive_link(args(body)?, sealer.as_ref()).await?)
2714 }
2715 "open_archive_token" => {
2716 let sealer = env.secret("REPOS_KEY").ok().and_then(|key| g1t_secrets::Sealer::new(&key.to_string()));
2717 reply(&repos.open_archive_token(args(body)?, sealer.as_ref()).await?)
2718 }
2719 "changed_files" => reply(&repos.changed_files(args(body)?).await?),
2720 "read_blobs" => reply(&repos.read_blobs(args(body)?).await?),
2721 // Services only: what the Composer registry builds packages from.
2722 "refs" => reply(&repos.refs_of(args(body)?).await?),
2723 "raw_file" => reply(&repos.raw_file(args(body)?).await?),
2724 "raw_blobs" => reply(&repos.raw_blobs(args(body)?).await?),
2725 "visibility" => {
2726 let a: g1t_contracts::repos::VisibilityArgs = args(body)?;
2727 reply(&repos.registry.visibility(&a.paths).await?)
2728 }
2729 "storage" => reply(&repos.registry.storage().await?),
2730 "git_operations" => {
2731 let a: GitOperationsArgs = args(body)?;
2732 reply(&git_ops::totals(&repos.registry.db, &a.month, a.since.as_deref(), a.namespace.as_deref().map(str::to_lowercase).as_deref()).await?)
2733 }
2734 // Identity, once: who created each repository (members.rs there).
2735 "repo_creators" => {
2736 let a: AllIdsArgs = args(body)?;
2737 let limit = a.limit.clamp(1, 500);
2738 let repos = repos.registry.creators_after(a.after.as_deref(), limit).await?;
2739 let next = (repos.len() == limit as usize).then(|| repos.last().map(|repo| repo.id.clone())).flatten();
2740 reply(&CreatorPage { repos, next })
2741 }
2742 "all_ids" => {
2743 let a: AllIdsArgs = args(body)?;
2744 let limit = a.limit.clamp(1, 500);
2745 let ids = repos.registry.ids_after(a.after.as_deref(), limit).await?;
2746 let next = (ids.len() == limit as usize).then(|| ids.last().cloned()).flatten();
2747 reply(&IdPage { ids, next })
2748 }
2749 // The raw meters of the git store, for reconciling with Cloudflare
2750 // (meters.rs, scripts/ops/gitstore-usage.mjs).
2751 "artifacts_usage" => {
2752 let a: meters::UsageArgs = args(body)?;
2753 reply(&meters::usage(&repos.registry.db, &a).await?)
2754 }
2755 "operation_mapping" => reply(&meters::read_mapping(&repos.registry.db).await?),
2756 // Billing: the workspace each pull request's working copy is counted
2757 // for, so Cloudflare's own count of `pulls--<id>` shares out too.
2758 "pull_owners" => {
2759 #[derive(serde::Deserialize)]
2760 struct PullOwnersArgs {
2761 pulls: Vec<String>,
2762 }
2763 let a: PullOwnersArgs = args(body)?;
2764 let pulls: Vec<String> = a.pulls.into_iter().take(500).collect();
2765 reply(&serde_json::json!({ "owners": meters::pull_owners(&repos.registry.db, &pulls).await? }))
2766 }
2767 // Services only: which meters are operations, changed without a deploy.
2768 "set_operation_mapping" => {
2769 let row: meters::MappingRow = args(body)?;
2770 meters::set_mapping(&repos.registry.db, &row, &rfc3339(now_ms())).await?;
2771 reply(&meters::read_mapping(&repos.registry.db).await?)
2772 }
2773 // Backups (backups.rs): the runner's sweep claims queued ones, and
2774 // each sandbox, through the API, asks for its job and says how it went.
2775 "claim_backups" => {
2776 let a: g1t_contracts::backups::ClaimBackupsArgs = args(body)?;
2777 let blobs = backups::storage(&env);
2778 reply(&backups::claim(&repos.registry.db, blobs.as_ref(), &a, now_ms()).await?)
2779 }
2780 "backup_spec" => match backups::storage(&env) {
2781 Some(blobs) => {
2782 let a: g1t_contracts::backups::BackupJobArgs = args(body)?;
2783 let every = backups::Settings::from_env(&env).full_every;
2784 reply(&backups::spec(&repos.registry, &blobs, &repos.store, &a, every, now_ms()).await?)
2785 }
2786 None => reply(&backups_off::<bool>()),
2787 },
2788 "backup_complete" => match backups::storage(&env) {
2789 Some(blobs) => reply(&backups::complete(&repos.registry, &blobs, &args(body)?, now_ms()).await?),
2790 None => reply(&backups_off::<bool>()),
2791 },
2792 "backup_fail" => match backups::storage(&env) {
2793 Some(blobs) => reply(&backups::fail(&repos.registry.db, &blobs, &args(body)?).await?),
2794 None => reply(&backups_off::<bool>()),
2795 },
2796 // How the git store has been answering, for the status page.
2797 "store_health" => {
2798 let a: meters::HealthArgs = args(body)?;
2799 reply(&meters::health(&repos.registry.db, &a).await?)
2800 }
2801 // Where repositories may be kept, for a workspace's settings.
2802 "storage_options" => reply(&repos.storage_options()),
2803 // Services and operators only: how each namespace stands, and
2804 // moving a repository between them (namespaces.rs, moves.rs).
2805 "namespaces" => reply(&repos.standings().await?),
2806 "move_repository" => reply(&repos.move_repository(args(body)?).await?),
2807 "repository_moves" => {
2808 let a: moves::ListMovesArgs = args(body)?;
2809 reply(&repos.registry.moves(a.limit.unwrap_or(50)).await?)
2810 }
2811 _ => Response::error("Unknown method", 404),
2812 } }
2813 .await;
2814 // The git store is busy: said in words, with when to try again.
2815 let answered = match answered {
2816 Err(error) => match resilience::busy(&error.to_string()) {
2817 Some(busy) if OUTCOME_READS.contains(&method.as_str()) => {
2818 reply(&Outcome::<()>::fail(FailureCode::Conflict, busy.message().trim()))
2819 }
2820 Some(busy) => {
2821 let response = Response::error(busy.message(), 503)?;
2822 response.headers().set("retry-after", &busy.retry_after.to_string())?;
2823 Ok(response)
2824 }
2825 None => Err(error),
2826 },
2827 answered => answered,
2828 };
2829 repos.deferred.hand_over(&ctx);
2830 flush_later(&env, &ctx);
2831 served.finish(answered)
2832}
2833
2834/// The nightly cron in wrangler.jsonc: tonight's backups are queued.
2835const BACKUP_CRON: &str = "53 2 * * *";
2836
2837/// The hourly sweep: deleted repositories whose time to be restored has
2838/// passed are purged. See lifecycle.rs. And, at [`BACKUP_CRON`], the
2839/// repositories whose refs moved are queued for a backup (backups.rs).
2840#[event(scheduled)]
2841async fn scheduled(event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
2842 let repos = match service(&env) {
2843 Ok(repos) => repos,
2844 Err(error) => {
2845 worker::console_error!("repos: the sweep could not start: {error}");
2846 return;
2847 }
2848 };
2849 if event.cron() == BACKUP_CRON {
2850 let Some(blobs) = backups::storage(&env) else { return };
2851 match backups::nightly(&repos.registry.db, &blobs, backups::Settings::from_env(&env), now_ms()).await {
2852 Ok(night) => worker::console_log!("repos: queued {} backups, removed {} of purged repositories", night.queued, night.pruned),
2853 Err(error) => worker::console_error!("repos: backups could not be queued: {error}"),
2854 }
2855 return;
2856 }
2857 match repos.purge_due(PurgeDueArgs::default()).await {
2858 Ok(0) => {}
2859 Ok(count) => worker::console_log!("repos: purged {count} deleted repositories"),
2860 Err(error) => worker::console_error!("repos: the purge sweep failed: {error}"),
2861 }
2862 // Pull requests' working copies whose time has come (forks.rs).
2863 match repos.retire_due().await {
2864 Ok(0) => {}
2865 Ok(count) => worker::console_log!("repos: removed {count} pull request working copies"),
2866 Err(error) => worker::console_error!("repos: the working copy sweep failed: {error}"),
2867 }
2868 // Repositories moving between namespaces, and old copies (moves.rs).
2869 match repos.run_moves().await {
2870 Ok(0) => {}
2871 Ok(count) => worker::console_log!("repos: moved {count} repositories between namespaces"),
2872 Err(error) => worker::console_error!("repos: the move sweep failed: {error}"),
2873 }
2874 meters::flush(&repos.registry.db).await;
2875}
2876
2877/// Events from the bus. A workspace's rename: its repositories move to the
2878/// workspace's current slug, asked of identity by id, so a repeated or late
2879/// delivery lands in the same place; their git store keys stay as they
2880/// were. A workspace's deletion: its repositories are deleted with it,
2881/// restored with it, or purged with it.
2882#[event(queue)]
2883async fn queue(batch: MessageBatch<Event>, env: Env, ctx: Context) -> Result<()> {
2884 let registry = Registry { db: env.d1("DB")? };
2885 let identity = env.service("IDENTITY")?;
2886 let handled = handle_events(&batch, &env, &registry, &identity).await;
2887 flush_later(&env, &ctx);
2888 handled
2889}
2890
2891/// Each event is acknowledged or retried on its own, so one that fails is
2892/// tried again without the others before and after it running twice.
2893async fn handle_events(batch: &MessageBatch<Event>, env: &Env, registry: &Registry, identity: &Fetcher) -> Result<()> {
2894 for message in batch.messages()? {
2895 match handle_event(message.body(), env, registry, identity).await {
2896 Ok(()) => message.ack(),
2897 Err(error) => {
2898 worker::console_error!("repos: event {} failed: {error}", message.body().id);
2899 message.retry();
2900 }
2901 }
2902 }
2903 Ok(())
2904}
2905
2906async fn handle_event(event: &Event, env: &Env, registry: &Registry, identity: &Fetcher) -> Result<()> {
2907 // A pull request merged, closed or reopened: its working copy is
2908 // kept or let go (forks.rs).
2909 if let Some(change) = forks::pull_change(&event.kind) {
2910 let Some(pull_id) = forks::pull_id_of(&event.data) else {
2911 worker::console_error!("{} {} names no pull request", event.kind, event.id);
2912 return Ok(());
2913 };
2914 let repos = service(env)?;
2915 match change {
2916 forks::PullChange::Settled => repos.pull_settled(&pull_id).await?,
2917 forks::PullChange::Reopened => repos.pull_reopened(&pull_id).await?,
2918 }
2919 return Ok(());
2920 }
2921 // A workspace deleted, restored or purged: its repositories go with
2922 // it, come back with it, or are purged with it (lifecycle.rs).
2923 if event.kind == "workspace.deleting" {
2924 match serde_json::from_value::<WorkspaceDeleting>(event.data.clone()) {
2925 Ok(deleting) => service(env)?.delete_with_workspace(&deleting, &protected_workspaces(env)).await?,
2926 Err(_) => worker::console_error!("workspace.deleting {} could not be read", event.id),
2927 }
2928 return Ok(());
2929 }
2930 if event.kind == "workspace.restored" {
2931 match serde_json::from_value::<WorkspaceRestored>(event.data.clone()) {
2932 Ok(restored) => service(env)?.restore_with_workspace(&restored).await?,
2933 Err(_) => worker::console_error!("workspace.restored {} could not be read", event.id),
2934 }
2935 return Ok(());
2936 }
2937 if event.kind == "workspace.deleted" {
2938 match serde_json::from_value::<WorkspaceDeleted>(event.data.clone()) {
2939 Ok(deleted) => service(env)?.purge_workspace(&deleted, &protected_workspaces(env)).await?,
2940 Err(_) => worker::console_error!("workspace.deleted {} could not be read", event.id),
2941 }
2942 return Ok(());
2943 }
2944 // An account deleted or restored: kept contributors that name it
2945 // (or ghost, for a restore) are worked out again, so it shows as
2946 // ghost for its 30 days, and as itself again if restored.
2947 if let Some(needle) = stats::shown_differently(&event.kind, &event.data) {
2948 let db = env.d1("DB")?;
2949 if let Err(error) = stats::rework_naming(&db, &needle).await {
2950 worker::console_error!("{} {}: contributors not marked to be counted again: {error}", event.kind, event.id);
2951 }
2952 return Ok(());
2953 }
2954 if event.kind != "workspace.renamed" {
2955 return Ok(());
2956 }
2957 let Ok(renamed) = serde_json::from_value::<WorkspaceRenamed>(event.data.clone()) else {
2958 worker::console_error!("workspace.renamed {} could not be read", event.id);
2959 return Ok(());
2960 };
2961 let names: HashMap<String, String> = g1t_kit::call(
2962 identity,
2963 "usernames",
2964 &g1t_contracts::identity::UsernamesArgs {
2965 ids: vec![renamed.workspace_id.clone()],
2966 },
2967 )
2968 .await?;
2969 let current = names
2970 .get(&renamed.workspace_id)
2971 .cloned()
2972 .unwrap_or_else(|| renamed.to.clone());
2973 let left = registry
2974 .rename_namespace(&renamed.stale_slugs(&current), &current)
2975 .await?;
2976 if left > 0 {
2977 worker::console_error!(
2978 "{left} repositories stayed under {} or {}: {current} already has repositories of the same names",
2979 renamed.from,
2980 renamed.to
2981 );
2982 }
2983 Ok(())
2984}
2985
2986/// The workspaces whose repositories never go with a deletion, whatever is
2987/// published: `PROTECTED_WORKSPACES` if set here, and Flagon's always.
2988fn protected_workspaces(env: &Env) -> Vec<String> {
2989 let configured = env.var("PROTECTED_WORKSPACES").ok().map(|v| v.to_string());
2990 g1t_contracts::identity::protected_names(configured.as_deref())
2991}
2992
2993/// What a token's own limits say about git on the repository `repo`
2994/// (`owner/name`), before anyone's role is asked: why it is refused, or
2995/// `None`. `source` is the repository a pull request's working copy at
2996/// `repo` belongs to, which a workflow job's token reaches too. `public`
2997/// is whether anyone may read it, and `exists` whether there is one.
2998///
2999/// A job's token and a deploy key reach their own repository only. Its
3000/// scopes decide the rest: `code:read` to read a private repository,
3001/// `code:write` to push, which a read-only deploy key never has. A deploy
3002/// key never makes a repository by pushing to an empty address.
3003pub(crate) fn git_token_refusal(
3004 access: &g1t_contracts::scopes::TokenAccess,
3005 repo: &str,
3006 source: Option<&str>,
3007 write: bool,
3008 public: bool,
3009 exists: bool,
3010) -> Option<String> {
3011 if let Some(refused) = g1t_contracts::scopes::decide_repo(access, repo)
3012 && !source.is_some_and(|source| access.reaches(source))
3013 {
3014 return Some(refused.reason.unwrap_or_default());
3015 }
3016 let decision = g1t_contracts::scopes::decide_git(access, write, public);
3017 if !decision.allowed {
3018 return Some(decision.reason.unwrap_or_default());
3019 }
3020 if access.deploy_key.is_some() && !exists {
3021 return Some(format!("This deploy key is for {repo}, which is not there any more."));
3022 }
3023 None
3024}
3025
3026/// The repository a push to a path that does not exist yet creates: private,
3027/// so nothing pushed by mistake is published. An owner makes it public on
3028/// purpose (`POST /repos/{owner}/{repo}/visibility`).
3029fn push_to_create(owner: &User, path: &RepoPath) -> CreateArgs {
3030 CreateArgs {
3031 owner: owner.clone(),
3032 namespace: path.namespace.clone(),
3033 name: path.name.clone(),
3034 description: None,
3035 is_private: true,
3036 import_url: None,
3037 import_token: None,
3038 }
3039}
3040
3041/// Whether `delete_branch` may remove `branch`: one of g1t's own
3042/// (`g1t-…`), or one whose tip the caller names, such as a dependency
3043/// update's branch after its pull request closed.
3044fn deletable_branch(branch: &str, head: Option<&str>) -> bool {
3045 !branch.is_empty() && (branch.starts_with(G1T_BRANCH_PREFIX) || head.is_some_and(|head| !head.is_empty()))
3046}
3047
3048#[cfg(test)]
3049mod delete_branch_tests {
3050 use super::deletable_branch;
3051
3052 #[test]
3053 fn only_g1t_branches_or_a_named_tip_are_deleted() {
3054 assert!(deletable_branch("g1t-queue-12", None));
3055 assert!(!deletable_branch("g1t/security/sharp-0.35.5", None));
3056 assert!(deletable_branch("g1t/security/sharp-0.35.5", Some("abc123")));
3057 assert!(!deletable_branch("feature", Some("")));
3058 assert!(!deletable_branch("", Some("abc123")));
3059 }
3060}
3061
3062#[cfg(test)]
3063mod announced_refs_tests {
3064 use super::*;
3065
3066 fn pushed(git_ref: String) -> git_http::Pushed {
3067 git_http::Pushed { git_ref, before: None, after: "abc".into() }
3068 }
3069
3070 fn refs(announced: Vec<&git_http::Pushed>) -> Vec<&str> {
3071 announced.into_iter().map(|pushed| pushed.git_ref.as_str()).collect()
3072 }
3073
3074 #[test]
3075 fn a_few_tags_are_announced_and_many_are_not() {
3076 let few: Vec<_> = (1..=3).map(|n| pushed(format!("refs/tags/v{n}"))).chain([pushed("refs/heads/main".into())]).collect();
3077 assert_eq!(refs(announced_refs(&few, "main")), ["refs/heads/main", "refs/tags/v1", "refs/tags/v2", "refs/tags/v3"]);
3078 let many: Vec<_> = (1..=10_000).map(|n| pushed(format!("refs/tags/v{n}"))).chain([pushed("refs/heads/main".into())]).collect();
3079 assert_eq!(refs(announced_refs(&many, "main")), ["refs/heads/main"]);
3080 }
3081
3082 #[test]
3083 fn past_the_branch_cap_only_the_default_branch_is_announced() {
3084 let at_cap: Vec<_> = (0..MAX_PUSH_BRANCH_EVENTS).map(|n| pushed(format!("refs/heads/b{n}"))).collect();
3085 assert_eq!(announced_refs(&at_cap, "main").len(), MAX_PUSH_BRANCH_EVENTS);
3086 let over: Vec<_> = (0..=MAX_PUSH_BRANCH_EVENTS)
3087 .map(|n| pushed(format!("refs/heads/b{n}")))
3088 .chain([pushed("refs/heads/main".into())])
3089 .collect();
3090 assert_eq!(refs(announced_refs(&over, "main")), ["refs/heads/main"]);
3091 assert!(announced_refs(&over, "trunk").is_empty());
3092 }
3093}
3094
3095#[cfg(test)]
3096mod push_to_create_tests {
3097 use super::*;
3098
3099 #[test]
3100 fn a_pushed_repository_starts_private() {
3101 let owner: User = serde_json::from_value(serde_json::json!({ "id": "usr_1", "username": "ada" })).unwrap();
3102 let args = push_to_create(&owner, &RepoPath { namespace: "acme".into(), name: "site".into() });
3103 assert!(args.is_private);
3104 assert_eq!((args.namespace.as_str(), args.name.as_str()), ("acme", "site"));
3105 }
3106}
3107
3108#[cfg(test)]
3109mod deploy_key_git_tests {
3110 use super::*;
3111 use g1t_contracts::deploy_keys;
3112
3113 fn key(read_only: bool) -> User {
3114 deploy_keys::principal("wsp_acme", "acme", deploy_keys::access("dk_1", "CI", "acme/rocket", read_only))
3115 }
3116
3117 fn rocket(private: bool) -> Repo {
3118 serde_json::from_value(serde_json::json!({
3119 "id": "rep_rocket",
3120 "namespace": "acme",
3121 "name": "rocket",
3122 "description": null,
3123 "isPrivate": private,
3124 "ownerId": "usr_owner",
3125 "defaultBranch": "main",
3126 "forkOf": null,
3127 "protected": false,
3128 "createdAt": "",
3129 }))
3130 .unwrap()
3131 }
3132
3133 fn refusal(user: &User, repo: &str, write: bool, exists: bool) -> Option<String> {
3134 git_token_refusal(user.token.as_deref().unwrap(), repo, None, write, false, exists)
3135 }
3136
3137 #[test]
3138 fn a_read_only_deploy_key_clones_its_repository_and_never_pushes() {
3139 let user = key(true);
3140 assert_eq!(refusal(&user, "acme/rocket", false, true), None);
3141 assert!(refusal(&user, "acme/rocket", true, true).unwrap().contains("read-only"));
3142 // Its role is a workspace token's: it reads a private repository.
3143 assert!(registry::can_read(&rocket(true), &Some(user)));
3144 }
3145
3146 #[test]
3147 fn a_deploy_key_with_write_access_pushes_to_its_repository() {
3148 let user = key(false);
3149 assert_eq!(refusal(&user, "acme/rocket", true, true), None);
3150 assert!(registry::can_write(&rocket(true), &Some(user)));
3151 }
3152
3153 #[test]
3154 fn a_deploy_key_reaches_no_other_repository() {
3155 let user = key(false);
3156 for other in ["acme/booster", "other/rocket"] {
3157 for write in [false, true] {
3158 let why = refusal(&user, other, write, true).expect(other);
3159 assert!(why.contains("deploy key is for acme/rocket"), "{why}");
3160 }
3161 }
3162 // Not even a pull request's working copy of another repository.
3163 let token = user.token.as_deref().unwrap();
3164 assert!(git_token_refusal(token, "pulls/pr_1", Some("acme/booster"), false, false, true).is_some());
3165 }
3166
3167 #[test]
3168 fn a_deploy_key_never_creates_a_repository() {
3169 let why = refusal(&key(false), "acme/rocket", true, false).unwrap();
3170 assert!(why.contains("not there"), "{why}");
3171 }
3172}