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