Skip to content

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

g1t.sh
3,211 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 Some(new) = land::checked_commit(&new, a.expected_commit.as_deref()) else {
1605 return Ok(Outcome::fail(FailureCode::Conflict, "The source branch changed after it was checked. Test it again before landing."));
1606 };
1607 let old = target_git
1608 .log(branch, 1)
1609 .await?
1610 .into_iter()
1611 .next()
1612 .map(|commit| commit.hash);
1613
1614 if old.as_deref() == Some(new.as_str()) {
1615 return Ok(Outcome::Ok(Landed {
1616 commit: new,
1617 previous: None,
1618 }));
1619 }
1620 if a.expected_commit.is_some() && old.is_some() {
1621 // A prior attempt may have published this exact commit and stopped
1622 // before recording the merge; another change can already follow it.
1623 let target_history = target_git.log(branch, MAX_ANCESTRY).await?;
1624 if descends_from(&target_git, &target_history, &new).await? {
1625 return Ok(Outcome::Ok(Landed { commit: new, previous: None }));
1626 }
1627 }
1628 // Moving the branch to a commit that does not descend from its
1629 // current head would discard whatever landed in between.
1630 if let Some(old) = &old
1631 && !descends_from(&source_git, &history, old).await?
1632 {
1633 let remedy = if from_fork {
1634 format!("Pull {branch} into the pull request's fork, push, and merge again.")
1635 } else {
1636 format!("Merge {branch} into {source_branch}, push, and merge again.")
1637 };
1638 return Ok(Outcome::fail(
1639 FailureCode::Conflict,
1640 format!("{branch} has moved since this pull request was opened. {remedy}"),
1641 ));
1642 }
1643
1644 // For a branch the objects are already in the target; sending them
1645 // again is harmless and keeps one way of moving a ref.
1646 let source_access = source_git.access(Scope::Read).await?;
1647 let target_access = target_git.access(Scope::Write).await?;
1648 let pushed =
1649 land::fast_forward(&source_access, &target_access, branch, old.as_deref(), &new)
1650 .await?;
1651 self.refs_moved(&target.id).await;
1652 if let Err(reason) = pushed {
1653 // Most often another pull request landed between the check and the push.
1654 return Ok(Outcome::fail(
1655 FailureCode::Conflict,
1656 format!("{branch} could not be updated: {reason}"),
1657 ));
1658 }
1659 self.publish_push(
1660 &target,
1661 &format!("refs/heads/{branch}"),
1662 old.as_deref(),
1663 &new,
1664 Some(&a.actor),
1665 None,
1666 )
1667 .await?;
1668 Ok(Outcome::Ok(Landed {
1669 commit: new,
1670 previous: old,
1671 }))
1672 }
1673
1674 async fn compare(&self, a: CompareArgs) -> Result<Outcome<Comparison>> {
1675 let Some(repo) = self
1676 .visible(self.registry.by_id(&a.repo_id).await?, &a.viewer)
1677 .await?
1678 else {
1679 return Ok(not_found());
1680 };
1681 let git = self.read_git(&repo).await?;
1682 let head_ref = a.head.as_deref().unwrap_or(&repo.default_branch);
1683 // A pull request into another branch is compared from where it
1684 // left that branch.
1685 let base_branch = a.base_branch.clone();
1686 // The head's history is only searched when the base is worked out
1687 // from another branch.
1688 let depth = if a.base.is_some() || is_commit_hash(head_ref) { 1 } else { MAX_ANCESTRY };
1689 let history = git.log(head_ref, depth).await?;
1690 let Some(head) = history.first() else {
1691 return Ok(Outcome::fail(
1692 FailureCode::Conflict,
1693 "There are no commits to compare.",
1694 ));
1695 };
1696
1697 // Where the head's history meets the default branch of `against`,
1698 // or the branch asked for.
1699 let shared_with = async |against: &Repo| -> Result<Option<String>> {
1700 let against_git = self.read_git(against).await?;
1701 let branch = base_branch.as_deref().unwrap_or(&against.default_branch);
1702 let shared: HashSet<String> = against_git
1703 .log(branch, MAX_ANCESTRY)
1704 .await?
1705 .into_iter()
1706 .map(|commit| commit.hash)
1707 .collect();
1708 nearest_ancestor_in(&git, &history, &shared).await
1709 };
1710 let base = match (a.base, &repo.fork_of) {
1711 (Some(base), _) => Some(base),
1712 // A fork is compared with the last commit it shares with the
1713 // repository it came from.
1714 (None, Some(target_id)) => match self.registry.by_id(target_id).await? {
1715 Some(target) => shared_with(&target).await?,
1716 None => None,
1717 },
1718 // A branch, with the point where it left the default branch.
1719 // A single commit, with its first parent.
1720 (None, None) if is_commit_hash(head_ref) => head.parents.first().cloned(),
1721 (None, None) if head_ref != base_branch.as_deref().unwrap_or(&repo.default_branch) => {
1722 shared_with(&repo).await?
1723 }
1724 (None, None) => head.parents.first().cloned(),
1725 };
1726 let base_tree = match &base {
1727 Some(base) => git
1728 .log(base, 1)
1729 .await?
1730 .into_iter()
1731 .next()
1732 .map(|commit| commit.tree_hash),
1733 None => None,
1734 };
1735 // Only a few files of it, when asked: how the files a large
1736 // comparison lists without their lines are read, as a page reaches them.
1737 let only = a.paths.as_deref().map(|paths| &paths[..paths.len().min(MAX_COMPARED_PATHS)]);
1738 let (files, truncated) =
1739 diff::compare_trees(&git, base_tree.as_deref(), &head.tree_hash, only).await?;
1740 Ok(Outcome::Ok(Comparison {
1741 base,
1742 head: head.hash.clone(),
1743 files,
1744 truncated,
1745 }))
1746 }
1747
1748 /// Reports that `git_ref` of `repo` (a full ref) now points to `after`,
1749 /// moved by `actor` (marked when that was a workflow job's token).
1750 /// `commits` is how many the move brought, when the caller knows: one
1751 /// for a file committed on the site or a branch brought up to date.
1752 async fn publish_push(
1753 &self,
1754 repo: &Repo,
1755 git_ref: &str,
1756 before: Option<&str>,
1757 after: &str,
1758 actor: Option<&User>,
1759 commits: Option<u32>,
1760 ) -> Result<()> {
1761 let caused_by_job = actor.and_then(g1t_contracts::events::job_run_of).map(str::to_owned);
1762 self.publish_git_push(repo, git_ref, before, after, actor.map(|user| user.id.clone()), false, caused_by_job, commits)
1763 .await
1764 }
1765
1766 /// `publish_push`, saying whether the push reached the store without
1767 /// being scanned for secrets first.
1768 #[allow(clippy::too_many_arguments)]
1769 async fn publish_git_push(
1770 &self,
1771 repo: &Repo,
1772 git_ref: &str,
1773 before: Option<&str>,
1774 after: &str,
1775 actor: Option<String>,
1776 unscanned: bool,
1777 caused_by_job: Option<String>,
1778 commits: Option<u32>,
1779 ) -> Result<()> {
1780 self.publish(NewEvent {
1781 kind: "git.push",
1782 source: SOURCE,
1783 repo_id: Some(repo.id.clone()),
1784 actor,
1785 data: GitPush {
1786 repo_id: repo.id.clone(),
1787 git_ref: git_ref.to_owned(),
1788 before: before.map(str::to_owned),
1789 after: after.to_owned(),
1790 default_branch: git_ref.strip_prefix("refs/heads/")
1791 == Some(repo.default_branch.as_str()),
1792 unscanned,
1793 caused_by_job,
1794 commits,
1795 },
1796 })
1797 .await
1798 }
1799
1800 /// Git over HTTPS. Only what decides the answer happens before it:
1801 /// the repository, who is asking and whether they may, the free
1802 /// workspace limits, push protection, and the store's own answer. The
1803 /// audit entry and what a push changed are recorded once git has its
1804 /// answer. Each answer says how long its steps took (`Server-Timing`).
1805 async fn git_http(&self, request: Request, env: &Env, ctx: &Context) -> Result<Response> {
1806 let mut timing = git_http::Timing::start();
1807 let Some(git) = git_http::parse(&request.url()?) else {
1808 return Response::error("Not found", 404);
1809 };
1810 let response = match self.answer_git(request, &git, env, ctx, &mut timing).await {
1811 Ok(response) => response,
1812 // The git store is busy: git hears when to try again.
1813 Err(error) => match resilience::busy(&error.to_string()) {
1814 Some(busy) => git_http::busy_response(busy)?,
1815 None => return Err(error),
1816 },
1817 };
1818 timing.apply(response)
1819 }
1820
1821 async fn answer_git(
1822 &self,
1823 request: Request,
1824 git: &git_http::GitRequest,
1825 env: &Env,
1826 ctx: &Context,
1827 timing: &mut git_http::Timing,
1828 ) -> Result<Response> {
1829 let write = git.service == GitService::ReceivePack;
1830 let get = request.method() == Method::Get;
1831 let identity = env.service("IDENTITY")?;
1832 // The repository and the caller's credentials, at once. A fetch may
1833 // go by the row as read a moment ago, for the same clone's next
1834 // request; a push always reads it. Anonymous callers cost nothing.
1835 let lookup = async {
1836 if write {
1837 self.registry.by_path(&git.path).await
1838 } else {
1839 self.registry.by_path_recent(&git.path).await
1840 }
1841 };
1842 let (found, viewer) =
1843 futures_util::future::join(lookup, git_http::viewer(&request, &identity)).await;
1844 let mut found = found?;
1845 timing.mark("repo");
1846 // A workspace alias staff set (identity's aliases.rs: `g1t` for
1847 // `flagon-io`) is answered in place, as the repository under the
1848 // workspace's slug: pushes and some clients do not follow
1849 // redirects. Everything after this sees only the workspace's slug.
1850 let aliased = match found {
1851 Some(_) => None,
1852 None => git_http::aliased(git, &identity).await?,
1853 };
1854 if let Some(aliased) = &aliased {
1855 found = if write {
1856 self.registry.by_path(&aliased.path).await?
1857 } else {
1858 self.registry.by_path_recent(&aliased.path).await?
1859 };
1860 timing.mark("alias");
1861 }
1862 let git = aliased.as_ref().unwrap_or(git);
1863 if found.is_none() {
1864 // A workspace that was renamed: git follows a redirect when it
1865 // first asks for refs, and uses the new address from then on.
1866 // A repository transferred to another workspace: the same, to
1867 // its new path. Fetches and pushes both follow either.
1868 let url = request.url()?;
1869 let (renamed, moved) = futures_util::future::join(
1870 git_http::renamed(&url, &identity),
1871 self.registry.resolve_moved(&git.path),
1872 )
1873 .await;
1874 timing.mark("moved");
1875 if let Some(location) = renamed? {
1876 return git_http::moved(&location, get);
1877 }
1878 if let Some(now) = moved?
1879 && let Some(location) = git_http::transferred(&url, &now)
1880 {
1881 return git_http::moved(&location, get);
1882 }
1883 }
1884 let viewer = viewer?;
1885 // A person who has not confirmed their email address can do
1886 // nothing with git until they do: told so, not asked to sign in.
1887 if viewer.as_ref().is_some_and(g1t_contracts::User::awaits_confirmation) {
1888 let site = request.url()?.origin().ascii_serialization();
1889 return git_http::refuse(Outcome::<()>::fail(
1890 FailureCode::Forbidden,
1891 g1t_contracts::accounts::confirm_email_first(&site),
1892 ));
1893 }
1894 // A run credential is checked against its grants, then acts as the
1895 // person it works for. See run_access.rs.
1896 let (request, viewer, audit) = match self.admit_git(request, git, viewer, found.as_ref()).await? {
1897 run_access::Admitted::Go { request, viewer, entry } => (request, viewer, entry),
1898 run_access::Admitted::Refused(response) => return Ok(response),
1899 };
1900 let mut after = AfterGit {
1901 audit,
1902 status: 0,
1903 message: None,
1904 push: None,
1905 };
1906 let repo = match self.authorize_git(&git.path, &viewer, git.service, found).await? {
1907 Outcome::Ok(repo) => repo,
1908 refused => {
1909 let response = git_http::refuse(refused)?;
1910 after.ended(response.status_code(), None);
1911 after.spawn(env, ctx);
1912 return Ok(response);
1913 }
1914 };
1915 // A pull request's working copy removed after it closed is made
1916 // again before git uses it (forks.rs).
1917 self.live(&repo).await?;
1918 timing.mark("access");
1919 // Clones check out the default branch g1t keeps, which can have
1920 // changed since the store made the repository.
1921 let default_branch = repo.fork_of.is_none().then(|| repo.default_branch.clone());
1922 let key = store_key(&repo);
1923 let scope = if write { Scope::Write } else { Scope::Read };
1924 let mut request = request;
1925 let protocol = refs_cache::protocol(request.headers().get("git-protocol")?.as_deref());
1926 // A fetch's POST is read here, to tell an `ls-refs` from a fetch of
1927 // objects; the store would have it read in full anyway.
1928 let body = if !write && !get { Some(request.bytes().await?) } else { None };
1929 // What it asks the store, for the meters (meters.rs).
1930 let call = git_ops::classify(git.service, git.endpoint, get, body.as_deref());
1931 // Answers kept from the usual store may name refs the fallback
1932 // store does not have (fallback.rs): none are used, or kept.
1933 let fallback = self.store.on_fallback(&key);
1934 // An answer that lists refs may have been kept: see refs_cache.rs.
1935 let kept_key = refs_cache::kind(git, get, protocol, body.as_deref())
1936 .filter(|_| !fallback)
1937 .zip(refs_cache::usable(registry::refs_state(&repo.id), now_ms()))
1938 .map(|(kind, version)| {
1939 refs_cache::Key::new(&repo.id, version, default_branch.as_deref(), protocol, &kind)
1940 });
1941 // A fresh clone's pack may have been kept too: see pack_cache.rs.
1942 // Under the same refs version, so never across a change to them.
1943 let pack_key = self
1944 .packs
1945 .as_ref()
1946 .filter(|_| !fallback)
1947 .and_then(|_| {
1948 let encoding = request.headers().get("content-encoding").ok().flatten();
1949 pack_cache::cacheable(git, get, protocol, encoding.as_deref(), body.as_deref())
1950 })
1951 .zip(refs_cache::usable(registry::refs_state(&repo.id), now_ms()))
1952 .map(|(normalized, version)| pack_cache::Key::new(&repo.id, version, &normalized));
1953 // A kept answer and the free workspace limits, with a kept
1954 // credential and, for a push, what the repository holds looked up
1955 // alongside. A kept answer goes back without waiting for the
1956 // credential, which it does not need.
1957 let pushing = write && !get;
1958 let ((answer, pack, (limited, held)), kept_access) = {
1959 let shared = self.shared.as_deref();
1960 let answer_and_limits = std::pin::pin!(futures_util::future::join3(
1961 async {
1962 match &kept_key {
1963 Some(kept_key) => refs_cache::get(shared, kept_key).await,
1964 None => None,
1965 }
1966 },
1967 async {
1968 match (&pack_key, self.packs.as_deref()) {
1969 (Some(pack_key), Some(packs)) => pack_cache::get(packs, pack_key).await,
1970 _ => None,
1971 }
1972 },
1973 futures_util::future::join(self.git_limits(call, git, &repo, env), async {
1974 if pushing { Some(self.held(&repo).await) } else { None }
1975 }),
1976 ));
1977 let kept_access = std::pin::pin!(self.store.kept_access(&key, scope));
1978 match futures_util::future::select(answer_and_limits, kept_access).await {
1979 futures_util::future::Either::Left((first, kept_access)) => {
1980 let answered = first.0.is_some() || first.1.is_some() || matches!(first.2.0, Ok(Some(_)) | Err(_));
1981 (first, if answered { None } else { kept_access.await })
1982 }
1983 futures_util::future::Either::Right((kept_access, first)) => (first.await, kept_access),
1984 }
1985 };
1986 timing.mark("kept");
1987 // A kept pack first: it never reaches the store, so it is never an
1988 // operation, and a free workspace past its operation cap still gets
1989 // it. (The other limits are a push's, and a pack is only a fetch.)
1990 if let Some(kept) = pack {
1991 timing.note("pack", "hit");
1992 let sent = body.as_ref().map_or(0, |body| body.len() as u64);
1993 meters::record(pack_cache::HIT, &key, sent, kept.size);
1994 after.ended(200, None);
1995 after.spawn(env, ctx);
1996 return kept.response();
1997 }
1998 if let Some((response, status, message)) = limited? {
1999 after.ended(status, Some(message.to_owned()));
2000 after.spawn(env, ctx);
2001 return Ok(response);
2002 }
2003 if pack_key.is_some() {
2004 timing.note("pack", "miss");
2005 }
2006 if let (Some((entry, found)), Some(kept_key)) = (answer, &kept_key) {
2007 timing.note("refs", found.as_str());
2008 if found == refs_cache::Found::Shared {
2009 let (kept_key, entry) = (kept_key.clone(), entry.clone());
2010 ctx.wait_until(async move { refs_cache::keep_in_colo(&kept_key, &entry).await });
2011 }
2012 // Never reached the store: never an operation.
2013 meters::record(call.cached_meter(), &key, 0, entry.body.len() as u64);
2014 after.ended(200, None);
2015 after.spawn(env, ctx);
2016 return entry.response();
2017 }
2018 if kept_key.is_some() {
2019 timing.note("refs", "miss");
2020 }
2021 // An anonymous fetch the store is about to answer: an operation for
2022 // the repository's workspace, so limited per repository (limits.rs).
2023 if limits::counts_as_anonymous_fetch(call, viewer.is_none())
2024 && g1t_kit::limits::check(env, limits::ANONYMOUS_FETCH, repo.id.clone()).await.limited()
2025 {
2026 let response = limits::too_many_anonymous_fetches(&format!("{}/{}", repo.namespace, repo.name))?;
2027 after.ended(429, Some("Too many anonymous fetches.".to_owned()));
2028 after.spawn(env, ctx);
2029 return Ok(response);
2030 }
2031 // The store's credential: one made a moment ago, here or in another
2032 // isolate (see store.rs), or a new one.
2033 let access = match kept_access {
2034 Some((access, from)) => {
2035 timing.note("cred", from.as_str());
2036 access
2037 }
2038 None => {
2039 let access = self.store.mint_access(&key, scope).await?;
2040 timing.mark("mint");
2041 timing.note("cred", "mint");
2042 access
2043 }
2044 };
2045 // Should the store turn a kept credential down, a fetch's first
2046 // request is tried again with a new one; the requests after it then
2047 // have that one too.
2048 let again = if get { Some(request.clone()?) } else { None };
2049 // A push's checks: workflow files, the rules of the branches and
2050 // tags it changes, saying which rule and why (rules.rs), and push
2051 // protection, which refuses a push that adds a secret
2052 // (secret_scan.rs). See push_checks.rs.
2053 let checks = async |head: &[u8], whole: bool, scan: bool| self.check_receive(&repo, viewer.as_ref(), head, whole, scan).await;
2054 // What a push may bring (pack_limits.rs): the repository's size is
2055 // its own and its pull requests' working copies'.
2056 let limits = if let Some(held) = held {
2057 git_http::PushLimits {
2058 held,
2059 repo_limit: self.repo_limit,
2060 large: self.large_pushes,
2061 ..git_http::PushLimits::default()
2062 }
2063 } else {
2064 git_http::PushLimits::default()
2065 };
2066 let mut outcome = git_http::forward(
2067 request,
2068 body,
2069 git,
2070 &access,
2071 checks,
2072 default_branch.as_deref(),
2073 limits,
2074 timing,
2075 )
2076 .await?;
2077 let turned_down = matches!(
2078 &outcome,
2079 git_http::Push::Forwarded(forwarded) if matches!(forwarded.response.status_code(), 401 | 403)
2080 );
2081 if turned_down {
2082 self.store.forget_access(&key).await;
2083 if let Some(again) = again {
2084 let access = self.store.mint_access(&key, scope).await?;
2085 outcome = git_http::forward(
2086 again,
2087 None,
2088 git,
2089 &access,
2090 async |_: &[u8], _: bool, _: bool| Ok(push_checks::Checks::clear()),
2091 default_branch.as_deref(),
2092 git_http::PushLimits::default(),
2093 timing,
2094 )
2095 .await?;
2096 }
2097 }
2098 let forwarded =
2099 match outcome {
2100 git_http::Push::Forwarded(forwarded) => forwarded,
2101 git_http::Push::Refused(response) => {
2102 after.ended(403, Some("The push was declined by rules.".to_owned()));
2103 after.spawn(env, ctx);
2104 return Ok(response);
2105 }
2106 git_http::Push::Blocked(response) => {
2107 after.ended(403, Some("The push adds a secret.".to_owned()));
2108 after.spawn(env, ctx);
2109 return Ok(response);
2110 }
2111 git_http::Push::Declined(response, reason) => {
2112 after.ended(403, Some(format!("The push was declined: {reason}.")));
2113 after.spawn(env, ctx);
2114 return Ok(response);
2115 }
2116 };
2117 if forwarded.from_store {
2118 let received = forwarded
2119 .response
2120 .headers()
2121 .get("content-length")?
2122 .and_then(|length| length.parse().ok())
2123 .unwrap_or(0);
2124 meters::record(call.meter(), &key, forwarded.sent, received);
2125 }
2126 timing.mark("store");
2127 let mut response = forwarded.response;
2128 let status = response.status_code();
2129 if write && !get {
2130 // A push: the store has moved its refs once it has answered in
2131 // full, so the answer is read before the change is recorded, and
2132 // only then goes back. Whoever fetches after it sees the push.
2133 let headers = response.headers().clone();
2134 headers.delete("content-length")?;
2135 let report = response.bytes().await?;
2136 self.refs_moved(&repo.id).await;
2137 timing.mark("refs");
2138 response = Response::from_bytes(report)?.with_headers(headers).with_status(status);
2139 } else if let (Some(kept_key), 200) = (&kept_key, status) {
2140 // A miss: this answer is kept for the next to ask.
2141 let headers = response.headers().clone();
2142 headers.delete("content-length")?;
2143 let body = response.bytes().await?;
2144 if let Some(content_type) = headers.get("content-type")? {
2145 let entry = refs_cache::Entry { content_type, body: body.clone() };
2146 if entry.keepable() {
2147 let shared = self.shared.clone();
2148 let kept_key = kept_key.clone();
2149 ctx.wait_until(async move { refs_cache::keep(shared.as_deref(), &kept_key, &entry).await });
2150 }
2151 }
2152 response = Response::from_bytes(body)?.with_headers(headers).with_status(status);
2153 } else if let (Some(pack_key), Some(packs), true) = (&pack_key, &self.packs, forwarded.from_store) {
2154 // A fresh clone the bucket did not have: counted, and its pack
2155 // kept as it streams to git, when it is a whole one.
2156 meters::record(pack_cache::MISS, &key, forwarded.sent, 0);
2157 // Past the repository's limit on writes to the bucket, the pack
2158 // goes to git without being kept (limits.rs).
2159 if status == 200 && !g1t_kit::limits::check(env, limits::PACK_FILL, repo.id.clone()).await.limited() {
2160 let store_key = key.clone();
2161 let measured = Box::new(move |bytes: u64| meters::record_bytes(pack_cache::MISS, &store_key, 0, bytes));
2162 let (teed, filling) = pack_cache::tee(response, packs.clone(), pack_key, measured)?;
2163 response = teed;
2164 if let Some(filling) = filling {
2165 let pack_key = pack_key.clone();
2166 ctx.wait_until(async move {
2167 let filled = filling.await;
2168 if !matches!(filled, pack_cache::Filled::Kept { .. } | pack_cache::Filled::Abandoned) {
2169 worker::console_warn!("pack {} not kept: {filled:?}", pack_key.as_str());
2170 }
2171 });
2172 }
2173 }
2174 }
2175 after.ended(status, None);
2176 if status == 200 && (forwarded.pack_bytes > 0 || !forwarded.pushed.is_empty() || !forwarded.deleted.is_empty()) {
2177 after.push = Some(PushDone {
2178 repo,
2179 pushed: forwarded.pushed,
2180 deleted: forwarded.deleted,
2181 pack_bytes: forwarded.pack_bytes,
2182 caused_by_job: viewer.as_ref().and_then(g1t_contracts::events::job_run_of).map(str::to_owned),
2183 // The calendar credits people, never a job's, an agent's or a workspace's token.
2184 credit: viewer
2185 .as_ref()
2186 .filter(|user| user.kind == PrincipalKind::User && g1t_contracts::events::job_run_of(user).is_none())
2187 .map(|user| user.id.clone()),
2188 actor: viewer.map(|user: User| user.id),
2189 unscanned: forwarded.unscanned,
2190 });
2191 }
2192 after.spawn(env, ctx);
2193 Ok(response)
2194 }
2195
2196 /// The answer for a request a free workspace's limits stop, or a push
2197 /// to a full repository, with its status and reason for the audit log;
2198 /// `None` to go on.
2199 ///
2200 /// A clone, fetch or push is a git operation, which the git store
2201 /// charges g1t for: counted for billing once the answer has gone back
2202 /// (meters.rs), and a free workspace far past its share is slowed down
2203 /// rather than charged (see git_ops.rs). Whether it is past it is
2204 /// decided from counts this isolate already holds: the database is not
2205 /// asked on the way. A free workspace is never charged for private
2206 /// storage: once its private repositories hold the free amount, pushes
2207 /// to them stop, checked when a push begins so that git shows the
2208 /// reason. So do pushes to a repository at the store's size limit.
2209 async fn git_limits(
2210 &self,
2211 call: git_ops::GitCall,
2212 git: &git_http::GitRequest,
2213 repo: &Repo,
2214 env: &Env,
2215 ) -> Result<Option<(Response, u16, &'static str)>> {
2216 let namespace = git.path.namespace.to_lowercase();
2217 if meters::mapping_now().billable(call.meter()) > 0.0 {
2218 let now = now_ms();
2219 let hour = git_ops::hour_key(&rfc3339(now));
2220 let limits = git_ops::Limits::from_env(env);
2221 if let Some((month, hour_ops)) = git_ops::standing(&namespace, &hour, now)
2222 && git_ops::slow_down(month + 1, hour_ops + 1, limits.free_cap, limits.hourly)
2223 && git_ops::is_free_kept(env.service("BILLING").ok().as_ref(), &namespace).await
2224 {
2225 return Ok(Some((
2226 git_ops::too_many(&namespace, limits.free_cap, limits.hourly)?,
2227 429,
2228 "Too many git operations this hour.",
2229 )));
2230 }
2231 }
2232 if git.service == GitService::ReceivePack && git.endpoint == "info/refs" {
2233 let held = self.held(repo).await;
2234 if held >= self.repo_limit {
2235 let message = format!(
2236 "{}/{} 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",
2237 repo.namespace,
2238 repo.name,
2239 pack_limits::megabytes(held)
2240 );
2241 return Ok(Some((Response::error(message, 403)?, 403, "The repository is full.")));
2242 }
2243 }
2244 if git.service == GitService::ReceivePack && git.endpoint == "info/refs" && repo.is_private {
2245 let free = git_ops::free_private_bytes(env);
2246 let held = self.registry.private_bytes(&namespace).await.unwrap_or(0);
2247 if git_ops::storage_full(held, free)
2248 && git_ops::is_free(env.service("BILLING").ok().as_ref(), &namespace).await
2249 {
2250 return Ok(Some((
2251 git_ops::storage_full_response(&namespace, held, free)?,
2252 403,
2253 "Free private storage is full.",
2254 )));
2255 }
2256 }
2257 if git.service == GitService::ReceivePack && git.endpoint == "info/refs" && !repo.is_private {
2258 let free = git_ops::free_public_bytes(env);
2259 if free > 0 {
2260 let held = self.registry.public_bytes(&namespace).await.unwrap_or(0);
2261 if git_ops::public_storage_full(held, free)
2262 && git_ops::is_free(env.service("BILLING").ok().as_ref(), &namespace).await
2263 {
2264 return Ok(Some((
2265 git_ops::public_storage_full_response(&namespace, held, free)?,
2266 403,
2267 "Free public storage is full.",
2268 )));
2269 }
2270 }
2271 }
2272 Ok(None)
2273 }
2274
2275 /// What a repository and its pull requests' working copies hold, as
2276 /// g1t counts it: read for a push's first request, kept a minute for
2277 /// the rest of it.
2278 async fn held(&self, repo: &Repo) -> u64 {
2279 let root = repo.fork_of.clone().unwrap_or_else(|| repo.id.clone());
2280 let now = now_ms();
2281 if let Some(held) = HELD.with(|held| held.borrow().get(&root, now)) {
2282 return held;
2283 }
2284 let held = self.registry.stored_bytes(&root).await.unwrap_or(0).max(0) as u64;
2285 HELD.with(|kept| kept.borrow_mut().put(root, held, now));
2286 held
2287 }
2288
2289 /// A `git.deleted` event for each branch or tag a push deleted that is
2290 /// gone from the store: the store can refuse one ref and accept
2291 /// another. As many as a push announces of tags and branches it moves.
2292 async fn publish_deleted(
2293 &self,
2294 repo: &Repo,
2295 deleted: &[(String, String)],
2296 actor: Option<&str>,
2297 caused_by_job: Option<&str>,
2298 ) -> Result<()> {
2299 let stored = self.store.open(&store_key(repo)).await?;
2300 let is_branch = |git_ref: &str| git_ref.starts_with("refs/heads/");
2301 let tags = deleted.iter().filter(|(git_ref, _)| !is_branch(git_ref)).count();
2302 let announced = deleted
2303 .iter()
2304 .filter(|(git_ref, _)| is_branch(git_ref))
2305 .take(MAX_PUSH_BRANCH_EVENTS)
2306 .chain(deleted.iter().filter(|(git_ref, _)| !is_branch(git_ref) && tags <= MAX_PUSH_TAG_EVENTS));
2307 for (git_ref, before) in announced {
2308 let named = git_ref.strip_prefix("refs/heads/").unwrap_or(git_ref);
2309 let gone = match stored.log(named, 1).await {
2310 Ok(head) => head.is_empty(),
2311 Err(error) => {
2312 worker::console_error!("whether {git_ref} of {} was deleted is not known: {error}", repo.name);
2313 false
2314 }
2315 };
2316 if !gone {
2317 continue;
2318 }
2319 self.publish(NewEvent {
2320 kind: "git.deleted",
2321 source: SOURCE,
2322 repo_id: Some(repo.id.clone()),
2323 actor: actor.map(str::to_owned),
2324 data: GitDeleted {
2325 repo_id: repo.id.clone(),
2326 git_ref: git_ref.clone(),
2327 before: before.clone(),
2328 caused_by_job: caused_by_job.map(str::to_owned),
2329 },
2330 })
2331 .await?;
2332 }
2333 Ok(())
2334 }
2335
2336 /// What a push changed, recorded once git has its answer.
2337 async fn record_push(&self, push: PushDone) -> Result<()> {
2338 let PushDone {
2339 repo,
2340 pushed,
2341 deleted,
2342 pack_bytes,
2343 actor,
2344 unscanned,
2345 caused_by_job,
2346 credit,
2347 } = push;
2348 // What the push stored, for billing's storage meter. A failure only
2349 // leaves the count short.
2350 if pack_bytes > 0
2351 && let Err(error) = self.registry.add_stored_bytes(&repo, pack_bytes).await
2352 {
2353 worker::console_error!("stored bytes for {} not counted: {error}", repo.name);
2354 }
2355 if !deleted.is_empty() {
2356 self.publish_deleted(&repo, &deleted, actor.as_deref(), caused_by_job.as_deref()).await?;
2357 }
2358 if pushed.is_empty() {
2359 return Ok(());
2360 }
2361 // Artifacts' own push notifications are per repository, which does
2362 // not fit a repo per pull request, so the front end reports pushes
2363 // itself: one event for each branch that moved.
2364 let stored = self.store.open(&store_key(&repo)).await?;
2365 let announced = announced_refs(&pushed, &repo.default_branch);
2366 if announced.len() < pushed.len() {
2367 worker::console_log!(
2368 "push to {}: {} of {} refs announced",
2369 repo.name,
2370 announced.len(),
2371 pushed.len()
2372 );
2373 }
2374 for pushed in announced {
2375 // The store can refuse one ref and accept another, so each
2376 // branch is checked against where it actually is. A tag the
2377 // store cannot read back is taken as pushed.
2378 let moved = match pushed.branch() {
2379 Some(branch) => stored
2380 .log(branch, 1)
2381 .await?
2382 .first()
2383 .is_some_and(|commit| commit.hash == pushed.after),
2384 None => stored.log(&pushed.git_ref, 1).await.map_or(true, |head| {
2385 head.first().is_none_or(|commit| commit.hash == pushed.after)
2386 }),
2387 };
2388 // How many commits a branch push brought (push_commits.rs), for
2389 // the event and the person's contribution calendar. A log the
2390 // store cannot read leaves the event without a count and the
2391 // day short.
2392 let commits = match (moved, pushed.branch()) {
2393 (true, Some(_)) => match stored.log(&pushed.after, push_commits::MOST_PER_PUSH + 1).await {
2394 Ok(log) => Some(push_commits::new_commits(&log, pushed.before.as_deref())),
2395 Err(error) => {
2396 worker::console_error!("commits pushed to {} not counted: {error}", repo.name);
2397 None
2398 }
2399 },
2400 _ => None,
2401 };
2402 if let (Some(commits), Some(user_id), Some(branch)) = (commits, credit.as_deref(), pushed.branch())
2403 && push_commits::counts(branch, &repo.default_branch)
2404 && let Err(error) =
2405 push_commits::record(&self.registry.db, user_id, &repo.id, &rfc3339(now_ms())[..10], commits).await
2406 {
2407 worker::console_error!("commits pushed to {} not credited: {error}", repo.name);
2408 }
2409 if moved {
2410 self.publish_git_push(
2411 &repo,
2412 &pushed.git_ref,
2413 pushed.before.as_deref(),
2414 &pushed.after,
2415 actor.clone(),
2416 unscanned,
2417 caused_by_job.clone(),
2418 commits,
2419 )
2420 .await?;
2421 }
2422 }
2423 Ok(())
2424 }
2425}
2426
2427/// The most tags one push announces: a push of more (`git push --tags`
2428/// into a new repository, say) announces none of them, so it starts no
2429/// workflows or package reads, one per tag.
2430const MAX_PUSH_TAG_EVENTS: usize = 3;
2431/// The most branches one push announces. A push of more announces only
2432/// the default branch, if it moved, which the rest of g1t reads from.
2433const MAX_PUSH_BRANCH_EVENTS: usize = 1000;
2434
2435/// The refs of a push that are announced with a `git.push` event each.
2436/// Every ref is stored whatever this says; only the events are capped.
2437fn announced_refs<'a>(pushed: &'a [git_http::Pushed], default_branch: &str) -> Vec<&'a git_http::Pushed> {
2438 let (branches, tags): (Vec<&git_http::Pushed>, Vec<&git_http::Pushed>) =
2439 pushed.iter().partition(|pushed| pushed.branch().is_some());
2440 let mut announced = if branches.len() > MAX_PUSH_BRANCH_EVENTS {
2441 branches.into_iter().filter(|pushed| pushed.branch() == Some(default_branch)).collect()
2442 } else {
2443 branches
2444 };
2445 if tags.len() <= MAX_PUSH_TAG_EVENTS {
2446 announced.extend(tags);
2447 }
2448 announced
2449}
2450
2451/// A push the store accepted, to be recorded once git has its answer.
2452struct PushDone {
2453 repo: Repo,
2454 pushed: Vec<git_http::Pushed>,
2455 /// The branches and tags it asked to delete, each with the commit it
2456 /// pointed to.
2457 deleted: Vec<(String, String)>,
2458 pack_bytes: u64,
2459 actor: Option<String>,
2460 /// Too large to scan for secrets before it was stored.
2461 unscanned: bool,
2462 /// The run whose job's token pushed, if one did: its push starts no
2463 /// workflows.
2464 caused_by_job: Option<String>,
2465 /// The person credited with the push's commits on their contribution
2466 /// calendar: whoever pushed, when that is a person (push_commits.rs).
2467 credit: Option<String>,
2468}
2469
2470/// What a git request leaves for after its answer: its audit entry, with
2471/// how the request ended, and what a push changed.
2472struct AfterGit {
2473 audit: Option<Box<g1t_contracts::audit::NewAuditEntry>>,
2474 status: u16,
2475 message: Option<String>,
2476 push: Option<PushDone>,
2477}
2478
2479impl AfterGit {
2480 fn ended(&mut self, status: u16, message: Option<String>) {
2481 self.status = status;
2482 self.message = message;
2483 }
2484
2485 /// Does the work once the response is on its way. A failure is logged:
2486 /// git has already been told how its request went.
2487 fn spawn(self, env: &Env, ctx: &Context) {
2488 if self.audit.is_none() && self.push.is_none() {
2489 return;
2490 }
2491 let env = env.clone();
2492 ctx.wait_until(async move {
2493 let repos = match service(&env) {
2494 Ok(repos) => repos,
2495 Err(error) => {
2496 worker::console_error!("git request not recorded: {error}");
2497 return;
2498 }
2499 };
2500 repos.finish_git(self.audit, self.status, self.message).await;
2501 if let Some(push) = self.push
2502 && let Err(error) = repos.record_push(push).await
2503 {
2504 worker::console_error!("push not recorded: {error}");
2505 }
2506 repos.deferred.settle().await;
2507 });
2508 }
2509}
2510
2511fn service(env: &Env) -> Result<Repos<ArtifactsStore>> {
2512 let shared = shared::Shared::from_env(env).map(Rc::new);
2513 let deferred = Rc::new(store::Deferred::default());
2514 Ok(Repos {
2515 registry: Registry { db: env.d1("DB")? },
2516 store: ArtifactsStore::new(env, shared.clone(), deferred.clone())?,
2517 deferred,
2518 shared,
2519 packs: pack_cache::Packs::from_env(env).map(Rc::new),
2520 events: env.service("EVENTS")?,
2521 security: env.service("SECURITY").ok(),
2522 billing: env.service("BILLING").ok(),
2523 identity: env.service("IDENTITY").ok(),
2524 work: env.service("WORK").ok(),
2525 free_private_bytes: git_ops::free_private_bytes(env),
2526 fork_days: forks::retention_days(env),
2527 repo_limit: env
2528 .var("REPO_STORAGE_LIMIT_BYTES")
2529 .ok()
2530 .and_then(|value| value.to_string().parse().ok())
2531 .unwrap_or(pack_limits::DEFAULT_REPO_LIMIT_BYTES),
2532 large_pushes: git_http::LargePushes::from_var(env.var("LARGE_PUSHES").ok().map(|value| value.to_string()).as_deref()),
2533 placement: shards::Placement::from_vars(
2534 env.var("GITSTORE_NEW_REPOS").ok().map(|value| value.to_string()).as_deref(),
2535 env.var("GITSTORE_EU_NAMESPACE").ok().map(|value| value.to_string()).as_deref(),
2536 ),
2537 limits: shards::limits(env.var("GITSTORE_NAMESPACE_LIMITS").ok().map(|value| value.to_string()).as_deref()),
2538 })
2539}
2540
2541/// Writes what this isolate metered once the answer has gone back, every
2542/// few seconds at most: now, or once it is due, waiting in this request's
2543/// `wait_until` so nothing counted is left for a request that may never
2544/// come (meters.rs).
2545fn flush_later(env: &Env, ctx: &Context) {
2546 let Some(wait) = meters::plan_flush() else {
2547 return;
2548 };
2549 if let Ok(db) = env.d1("DB") {
2550 ctx.wait_until(async move { meters::flush_after(&db, wait).await });
2551 }
2552}
2553
2554/// `/backups/<job id>/parts/<number>`: the job and the part's number.
2555fn backup_part_path(path: &str) -> Option<(String, u16)> {
2556 let rest = path.strip_prefix("/backups/")?;
2557 let (job, number) = rest.split_once("/parts/")?;
2558 let number = number.parse::<u16>().ok()?;
2559 (!job.is_empty() && !job.contains('/')).then(|| (job.to_owned(), number))
2560}
2561
2562fn backups_off<T>() -> Outcome<T> {
2563 Outcome::fail(FailureCode::Conflict, "Backups are off on this installation: it has no storage for them.")
2564}
2565
2566/// One part of a backup's bundle, with the job's token in its header.
2567async fn backup_part(request: &mut Request, env: &Env, repos: &Repos<ArtifactsStore>, job_id: String, number: u16) -> Result<Response> {
2568 let Some(blobs) = backups::storage(env) else {
2569 return reply(&backups_off::<()>());
2570 };
2571 let token = request.headers().get(g1t_contracts::backups::TOKEN_HEADER)?.unwrap_or_default();
2572 let bytes = request.bytes().await?;
2573 let job = g1t_contracts::backups::BackupJobArgs { job_id, token };
2574 reply(&backups::part(&repos.registry.db, &blobs, &job, number, bytes).await?)
2575}
2576
2577#[cfg(test)]
2578mod backup_path_tests {
2579 use super::backup_part_path;
2580
2581 #[test]
2582 fn a_part_is_named_by_its_job_and_number() {
2583 assert_eq!(backup_part_path("/backups/bkp_1/parts/3"), Some(("bkp_1".to_owned(), 3)));
2584 assert_eq!(backup_part_path("/backups/bkp_1/parts/x"), None);
2585 assert_eq!(backup_part_path("/backups//parts/1"), None);
2586 assert_eq!(backup_part_path("/acme/rocket.git/info/refs"), None);
2587 }
2588}
2589
2590/// Read methods whose answer is an `Outcome`: when the git store is busy,
2591/// the site is told so in words instead of failing the page.
2592const OUTCOME_READS: [&str; 7] = ["tree", "blob", "log", "branches", "blame", "compare", "branch_drift"];
2593
2594#[event(fetch)]
2595async fn fetch(mut request: Request, env: Env, ctx: Context) -> Result<Response> {
2596 let mut repos = service(&env)?;
2597 // A part of a backup's bundle, as the API passes it on from the
2598 // sandbox: bytes, not JSON (backups.rs).
2599 if request.method() == Method::Put
2600 && let Some((job_id, number)) = backup_part_path(&request.path())
2601 {
2602 let answered = backup_part(&mut request, &env, &repos, job_id, number).await;
2603 repos.deferred.hand_over(&ctx);
2604 flush_later(&env, &ctx);
2605 return answered;
2606 }
2607 let Some(method) = rpc_method(&request) else {
2608 let answered = repos.git_http(request, &env, &ctx).await;
2609 repos.deferred.hand_over(&ctx);
2610 flush_later(&env, &ctx);
2611 return answered;
2612 };
2613 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
2614 // Git over HTTPS above always reads the primary.
2615 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
2616 repos.registry.db = db;
2617 let body: serde_json::Value = request.json().await?;
2618
2619 let answered = async { match method.as_str() {
2620 "get" => reply(&repos.get(args(body)?).await?),
2621 "get_by_id" => reply(&repos.get_by_id(args(body)?).await?),
2622 "readable" => {
2623 let a: ReadableArgs = args(body)?;
2624 reply(&repos.registry.readable(&a.ids, &a.viewer).await?)
2625 }
2626 // Work's contribution calendar: the commits a person pushed each
2627 // day, in repositories the viewer may read (push_commits.rs).
2628 "commit_days" => {
2629 let a: g1t_contracts::repos::CommitDaysArgs = args(body)?;
2630 reply(&push_commits::days(&repos.registry, &a.user_id, &a.since, &a.viewer).await?)
2631 }
2632 "public_namespaces" => {
2633 let a: PublicNamespacesArgs = args(body)?;
2634 reply(&repos.registry.public_namespaces(&a.owner_id).await?)
2635 }
2636 "path_by_id" => {
2637 let a: PathByIdArgs = args(body)?;
2638 reply(
2639 &repos
2640 .registry
2641 .by_id(&a.id)
2642 .await?
2643 .filter(|repo| repo.fork_of.is_none())
2644 .map(|repo| RepoPath {
2645 namespace: repo.namespace,
2646 name: repo.name,
2647 }),
2648 )
2649 }
2650 "list" => {
2651 let a: ListArgs = args(body)?;
2652 reply(
2653 &repos
2654 .registry
2655 .list(
2656 &a.viewer,
2657 a.query.as_deref(),
2658 a.namespace.as_deref(),
2659 a.member_only,
2660 )
2661 .await?,
2662 )
2663 }
2664 "create" => reply(&repos.create(args(body)?).await?),
2665 "transfer" => reply(&repos.transfer(args(body)?).await?),
2666 // A repository's lifecycle: see lifecycle.rs.
2667 "delete" => reply(&repos.delete(args(body)?).await?),
2668 "deleted" => reply(&repos.deleted(args(body)?).await?),
2669 "restore" => reply(&repos.restore(args(body)?).await?),
2670 "purge" => reply(&repos.purge(args(body)?).await?),
2671 "purge_due" => reply(&repos.purge_due(args(body)?).await?),
2672 "rename" => reply(&repos.rename(args(body)?).await?),
2673 "archive" => reply(&repos.archive(args(body)?).await?),
2674 "set_visibility" => reply(&repos.set_visibility(args(body)?).await?),
2675 "set_default_branch" => reply(&repos.set_default_branch(args(body)?).await?),
2676 "rename_branch" => reply(&repos.rename_branch(args(body)?).await?),
2677 "create_branch" => reply(&repos.create_branch(args(body)?).await?),
2678 "remove_branch" => reply(&repos.remove_branch(args(body)?).await?),
2679 "resolve_branch" => reply(&repos.resolve_branch(args(body)?).await?),
2680 "status_by_id" => reply(&repos.status_by_id(args(body)?).await?),
2681 "resolve_path" => {
2682 let a: ResolvePathArgs = args(body)?;
2683 reply(&repos.registry.resolve_moved(&a.path).await?)
2684 }
2685 "namespace_count" => {
2686 let a: NamespaceCountArgs = args(body)?;
2687 reply(&repos.registry.count_in(&a.namespace).await?)
2688 }
2689 "update" => reply(&repos.update(args(body)?).await?),
2690 "tree" => reply(&repos.tree(args(body)?).await?),
2691 "blob" => reply(&repos.blob(args(body)?).await?),
2692 "log" => reply(&repos.log(args(body)?).await?),
2693 "blame" => reply(&repos.blame(args(body)?).await?),
2694 "fork_for_pull" => reply(&repos.fork_for_pull(args(body)?).await?),
2695 "git_access" => reply(&repos.git_access(args(body)?).await?),
2696 "branches" => reply(&repos.branches(args(body)?).await?),
2697 "last_commits" => reply(&repos.last_commits(args(body)?).await?),
2698 "path_log" => reply(&repos.path_log(args(body)?).await?),
2699 "branch_drift" => reply(&repos.branch_drift(args(body)?).await?),
2700 "tags" => reply(&repos.tags(args(body)?).await?),
2701 // The About: what the Files page shows beside the files (about.rs).
2702 // What is kept behind the head is worked out again after the answer.
2703 "about" => {
2704 let (answer, refresh) = repos.about(args(body)?).await?;
2705 about::refresh_later(&env, &ctx, refresh);
2706 reply(&answer)
2707 }
2708 "languages" => {
2709 let (answer, refresh) = repos.languages(args(body)?).await?;
2710 about::refresh_later(&env, &ctx, refresh);
2711 reply(&answer)
2712 }
2713 "contributors" => {
2714 let (answer, refresh) = repos.contributors(args(body)?).await?;
2715 about::refresh_later(&env, &ctx, refresh);
2716 reply(&answer)
2717 }
2718 "license" => {
2719 let (answer, refresh) = repos.license(args(body)?).await?;
2720 about::refresh_later(&env, &ctx, refresh);
2721 reply(&answer)
2722 }
2723 "stars" => reply(&repos.stars(args(body)?).await?),
2724 "star" => reply(&repos.star(args(body)?).await?),
2725 "stargazers" => reply(&repos.stargazers(args(body)?).await?),
2726 "starred" => reply(&repos.starred(args(body)?).await?),
2727 "releases" => reply(&repos.releases(args(body)?).await?),
2728 "release" => reply(&repos.release(args(body)?).await?),
2729 "create_release" => reply(&repos.create_release(args(body)?).await?),
2730 "update_release" => reply(&repos.update_release(args(body)?).await?),
2731 "delete_release" => reply(&repos.delete_release(args(body)?).await?),
2732 "head" => reply(&repos.head(args(body)?).await?),
2733 "behind" => reply(&repos.behind(args(body)?).await?),
2734 "divergence" => reply(&repos.divergence(args(body)?).await?),
2735 "land" => reply(&repos.land(args(body)?).await?),
2736 "update_pull_branch" => reply(&repos.update_pull_branch(args(body)?).await?),
2737 // Services only: a pull request's test merge commit (test_merge.rs).
2738 "pull_merge_ref" => reply(&repos.pull_merge_ref(args(body)?).await?),
2739 "delete_branch" => reply(&repos.delete_branch(args(body)?).await?),
2740 "commit_file" => reply(&repos.commit_file(args(body)?).await?),
2741 "compare" => reply(&repos.compare(args(body)?).await?),
2742 // Services only: a pull request's commits, as rules look at them (rules.rs).
2743 "inspect_commits" => reply(&repos.inspect_commits(args(body)?).await?),
2744 "scan_history" => reply(&repos.scan_history(args(body)?).await?),
2745 "find_lockfiles" => reply(&repos.find_lockfiles(args(body)?).await?),
2746 "match_pattern" => reply(&repos.match_pattern(args(body)?).await?),
2747 "check_secret" => reply(&repos.check_secret(args(body)?).await?),
2748 "list_files" => reply(&repos.list_files(args(body)?).await?),
2749 // The API's tarball and zipball links, and the site opening them (archive_links.rs).
2750 "archive_link" => {
2751 let sealer = env.secret("REPOS_KEY").ok().and_then(|key| g1t_secrets::Sealer::new(&key.to_string()));
2752 reply(&repos.archive_link(args(body)?, sealer.as_ref()).await?)
2753 }
2754 "open_archive_token" => {
2755 let sealer = env.secret("REPOS_KEY").ok().and_then(|key| g1t_secrets::Sealer::new(&key.to_string()));
2756 reply(&repos.open_archive_token(args(body)?, sealer.as_ref()).await?)
2757 }
2758 "changed_files" => reply(&repos.changed_files(args(body)?).await?),
2759 "read_blobs" => reply(&repos.read_blobs(args(body)?).await?),
2760 // Services only: what the Composer registry builds packages from.
2761 "refs" => reply(&repos.refs_of(args(body)?).await?),
2762 "raw_file" => reply(&repos.raw_file(args(body)?).await?),
2763 "raw_blobs" => reply(&repos.raw_blobs(args(body)?).await?),
2764 "visibility" => {
2765 let a: g1t_contracts::repos::VisibilityArgs = args(body)?;
2766 reply(&repos.registry.visibility(&a.paths).await?)
2767 }
2768 "storage" => reply(&repos.registry.storage().await?),
2769 "git_operations" => {
2770 let a: GitOperationsArgs = args(body)?;
2771 reply(&git_ops::totals(&repos.registry.db, &a.month, a.since.as_deref(), a.namespace.as_deref().map(str::to_lowercase).as_deref()).await?)
2772 }
2773 // Identity, once: who created each repository (members.rs there).
2774 "repo_creators" => {
2775 let a: AllIdsArgs = args(body)?;
2776 let limit = a.limit.clamp(1, 500);
2777 let repos = repos.registry.creators_after(a.after.as_deref(), limit).await?;
2778 let next = (repos.len() == limit as usize).then(|| repos.last().map(|repo| repo.id.clone())).flatten();
2779 reply(&CreatorPage { repos, next })
2780 }
2781 "all_ids" => {
2782 let a: AllIdsArgs = args(body)?;
2783 let limit = a.limit.clamp(1, 500);
2784 let ids = repos.registry.ids_after(a.after.as_deref(), limit).await?;
2785 let next = (ids.len() == limit as usize).then(|| ids.last().cloned()).flatten();
2786 reply(&IdPage { ids, next })
2787 }
2788 // The raw meters of the git store, for reconciling with Cloudflare
2789 // (meters.rs, scripts/ops/gitstore-usage.mjs).
2790 "artifacts_usage" => {
2791 let a: meters::UsageArgs = args(body)?;
2792 reply(&meters::usage(&repos.registry.db, &a).await?)
2793 }
2794 "operation_mapping" => reply(&meters::read_mapping(&repos.registry.db).await?),
2795 // Billing: the workspace each pull request's working copy is counted
2796 // for, so Cloudflare's own count of `pulls--<id>` shares out too.
2797 "pull_owners" => {
2798 #[derive(serde::Deserialize)]
2799 struct PullOwnersArgs {
2800 pulls: Vec<String>,
2801 }
2802 let a: PullOwnersArgs = args(body)?;
2803 let pulls: Vec<String> = a.pulls.into_iter().take(500).collect();
2804 reply(&serde_json::json!({ "owners": meters::pull_owners(&repos.registry.db, &pulls).await? }))
2805 }
2806 // Services only: which meters are operations, changed without a deploy.
2807 "set_operation_mapping" => {
2808 let row: meters::MappingRow = args(body)?;
2809 meters::set_mapping(&repos.registry.db, &row, &rfc3339(now_ms())).await?;
2810 reply(&meters::read_mapping(&repos.registry.db).await?)
2811 }
2812 // Backups (backups.rs): the runner's sweep claims queued ones, and
2813 // each sandbox, through the API, asks for its job and says how it went.
2814 "claim_backups" => {
2815 let a: g1t_contracts::backups::ClaimBackupsArgs = args(body)?;
2816 let blobs = backups::storage(&env);
2817 reply(&backups::claim(&repos.registry.db, blobs.as_ref(), &a, now_ms()).await?)
2818 }
2819 "backup_spec" => match backups::storage(&env) {
2820 Some(blobs) => {
2821 let a: g1t_contracts::backups::BackupJobArgs = args(body)?;
2822 let every = backups::Settings::from_env(&env).full_every;
2823 reply(&backups::spec(&repos.registry, &blobs, &repos.store, &a, every, now_ms()).await?)
2824 }
2825 None => reply(&backups_off::<bool>()),
2826 },
2827 "backup_complete" => match backups::storage(&env) {
2828 Some(blobs) => reply(&backups::complete(&repos.registry, &blobs, &args(body)?, now_ms()).await?),
2829 None => reply(&backups_off::<bool>()),
2830 },
2831 "backup_fail" => match backups::storage(&env) {
2832 Some(blobs) => reply(&backups::fail(&repos.registry.db, &blobs, &args(body)?).await?),
2833 None => reply(&backups_off::<bool>()),
2834 },
2835 // How the git store has been answering, for the status page.
2836 "store_health" => {
2837 let a: meters::HealthArgs = args(body)?;
2838 reply(&meters::health(&repos.registry.db, &a).await?)
2839 }
2840 // Where repositories may be kept, for a workspace's settings.
2841 "storage_options" => reply(&repos.storage_options()),
2842 // Services and operators only: how each namespace stands, and
2843 // moving a repository between them (namespaces.rs, moves.rs).
2844 "namespaces" => reply(&repos.standings().await?),
2845 "move_repository" => reply(&repos.move_repository(args(body)?).await?),
2846 "repository_moves" => {
2847 let a: moves::ListMovesArgs = args(body)?;
2848 reply(&repos.registry.moves(a.limit.unwrap_or(50)).await?)
2849 }
2850 _ => Response::error("Unknown method", 404),
2851 } }
2852 .await;
2853 // The git store is busy: said in words, with when to try again.
2854 let answered = match answered {
2855 Err(error) => match resilience::busy(&error.to_string()) {
2856 Some(busy) if OUTCOME_READS.contains(&method.as_str()) => {
2857 reply(&Outcome::<()>::fail(FailureCode::Conflict, busy.message().trim()))
2858 }
2859 Some(busy) => {
2860 let response = Response::error(busy.message(), 503)?;
2861 response.headers().set("retry-after", &busy.retry_after.to_string())?;
2862 Ok(response)
2863 }
2864 None => Err(error),
2865 },
2866 answered => answered,
2867 };
2868 repos.deferred.hand_over(&ctx);
2869 flush_later(&env, &ctx);
2870 served.finish(answered)
2871}
2872
2873/// The nightly cron in wrangler.jsonc: tonight's backups are queued.
2874const BACKUP_CRON: &str = "53 2 * * *";
2875
2876/// The hourly sweep: deleted repositories whose time to be restored has
2877/// passed are purged. See lifecycle.rs. And, at [`BACKUP_CRON`], the
2878/// repositories whose refs moved are queued for a backup (backups.rs).
2879#[event(scheduled)]
2880async fn scheduled(event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
2881 let repos = match service(&env) {
2882 Ok(repos) => repos,
2883 Err(error) => {
2884 worker::console_error!("repos: the sweep could not start: {error}");
2885 return;
2886 }
2887 };
2888 if event.cron() == BACKUP_CRON {
2889 let Some(blobs) = backups::storage(&env) else { return };
2890 match backups::nightly(&repos.registry.db, &blobs, backups::Settings::from_env(&env), now_ms()).await {
2891 Ok(night) => worker::console_log!("repos: queued {} backups, removed {} of purged repositories", night.queued, night.pruned),
2892 Err(error) => worker::console_error!("repos: backups could not be queued: {error}"),
2893 }
2894 return;
2895 }
2896 match repos.purge_due(PurgeDueArgs::default()).await {
2897 Ok(0) => {}
2898 Ok(count) => worker::console_log!("repos: purged {count} deleted repositories"),
2899 Err(error) => worker::console_error!("repos: the purge sweep failed: {error}"),
2900 }
2901 // Pull requests' working copies whose time has come (forks.rs).
2902 match repos.retire_due().await {
2903 Ok(0) => {}
2904 Ok(count) => worker::console_log!("repos: removed {count} pull request working copies"),
2905 Err(error) => worker::console_error!("repos: the working copy sweep failed: {error}"),
2906 }
2907 // Repositories moving between namespaces, and old copies (moves.rs).
2908 match repos.run_moves().await {
2909 Ok(0) => {}
2910 Ok(count) => worker::console_log!("repos: moved {count} repositories between namespaces"),
2911 Err(error) => worker::console_error!("repos: the move sweep failed: {error}"),
2912 }
2913 meters::flush(&repos.registry.db).await;
2914}
2915
2916/// Events from the bus. A workspace's rename: its repositories move to the
2917/// workspace's current slug, asked of identity by id, so a repeated or late
2918/// delivery lands in the same place; their git store keys stay as they
2919/// were. A workspace's deletion: its repositories are deleted with it,
2920/// restored with it, or purged with it.
2921#[event(queue)]
2922async fn queue(batch: MessageBatch<Event>, env: Env, ctx: Context) -> Result<()> {
2923 let registry = Registry { db: env.d1("DB")? };
2924 let identity = env.service("IDENTITY")?;
2925 let handled = handle_events(&batch, &env, &registry, &identity).await;
2926 flush_later(&env, &ctx);
2927 handled
2928}
2929
2930/// Each event is acknowledged or retried on its own, so one that fails is
2931/// tried again without the others before and after it running twice.
2932async fn handle_events(batch: &MessageBatch<Event>, env: &Env, registry: &Registry, identity: &Fetcher) -> Result<()> {
2933 for message in batch.messages()? {
2934 match handle_event(message.body(), env, registry, identity).await {
2935 Ok(()) => message.ack(),
2936 Err(error) => {
2937 worker::console_error!("repos: event {} failed: {error}", message.body().id);
2938 message.retry();
2939 }
2940 }
2941 }
2942 Ok(())
2943}
2944
2945async fn handle_event(event: &Event, env: &Env, registry: &Registry, identity: &Fetcher) -> Result<()> {
2946 // A pull request merged, closed or reopened: its working copy is
2947 // kept or let go (forks.rs).
2948 if let Some(change) = forks::pull_change(&event.kind) {
2949 let Some(pull_id) = forks::pull_id_of(&event.data) else {
2950 worker::console_error!("{} {} names no pull request", event.kind, event.id);
2951 return Ok(());
2952 };
2953 let repos = service(env)?;
2954 match change {
2955 forks::PullChange::Settled => repos.pull_settled(&pull_id).await?,
2956 forks::PullChange::Reopened => repos.pull_reopened(&pull_id).await?,
2957 }
2958 return Ok(());
2959 }
2960 // A workspace deleted, restored or purged: its repositories go with
2961 // it, come back with it, or are purged with it (lifecycle.rs).
2962 if event.kind == "workspace.deleting" {
2963 match serde_json::from_value::<WorkspaceDeleting>(event.data.clone()) {
2964 Ok(deleting) => service(env)?.delete_with_workspace(&deleting, &protected_workspaces(env)).await?,
2965 Err(_) => worker::console_error!("workspace.deleting {} could not be read", event.id),
2966 }
2967 return Ok(());
2968 }
2969 if event.kind == "workspace.restored" {
2970 match serde_json::from_value::<WorkspaceRestored>(event.data.clone()) {
2971 Ok(restored) => service(env)?.restore_with_workspace(&restored).await?,
2972 Err(_) => worker::console_error!("workspace.restored {} could not be read", event.id),
2973 }
2974 return Ok(());
2975 }
2976 if event.kind == "workspace.deleted" {
2977 match serde_json::from_value::<WorkspaceDeleted>(event.data.clone()) {
2978 Ok(deleted) => service(env)?.purge_workspace(&deleted, &protected_workspaces(env)).await?,
2979 Err(_) => worker::console_error!("workspace.deleted {} could not be read", event.id),
2980 }
2981 return Ok(());
2982 }
2983 // An account deleted or restored: kept contributors that name it
2984 // (or ghost, for a restore) are worked out again, so it shows as
2985 // ghost for its 30 days, and as itself again if restored.
2986 if let Some(needle) = stats::shown_differently(&event.kind, &event.data) {
2987 let db = env.d1("DB")?;
2988 if let Err(error) = stats::rework_naming(&db, &needle).await {
2989 worker::console_error!("{} {}: contributors not marked to be counted again: {error}", event.kind, event.id);
2990 }
2991 return Ok(());
2992 }
2993 if event.kind != "workspace.renamed" {
2994 return Ok(());
2995 }
2996 let Ok(renamed) = serde_json::from_value::<WorkspaceRenamed>(event.data.clone()) else {
2997 worker::console_error!("workspace.renamed {} could not be read", event.id);
2998 return Ok(());
2999 };
3000 let names: HashMap<String, String> = g1t_kit::call(
3001 identity,
3002 "usernames",
3003 &g1t_contracts::identity::UsernamesArgs {
3004 ids: vec![renamed.workspace_id.clone()],
3005 },
3006 )
3007 .await?;
3008 let current = names
3009 .get(&renamed.workspace_id)
3010 .cloned()
3011 .unwrap_or_else(|| renamed.to.clone());
3012 let left = registry
3013 .rename_namespace(&renamed.stale_slugs(&current), &current)
3014 .await?;
3015 if left > 0 {
3016 worker::console_error!(
3017 "{left} repositories stayed under {} or {}: {current} already has repositories of the same names",
3018 renamed.from,
3019 renamed.to
3020 );
3021 }
3022 Ok(())
3023}
3024
3025/// The workspaces whose repositories never go with a deletion, whatever is
3026/// published: `PROTECTED_WORKSPACES` if set here, and Flagon's always.
3027fn protected_workspaces(env: &Env) -> Vec<String> {
3028 let configured = env.var("PROTECTED_WORKSPACES").ok().map(|v| v.to_string());
3029 g1t_contracts::identity::protected_names(configured.as_deref())
3030}
3031
3032/// What a token's own limits say about git on the repository `repo`
3033/// (`owner/name`), before anyone's role is asked: why it is refused, or
3034/// `None`. `source` is the repository a pull request's working copy at
3035/// `repo` belongs to, which a workflow job's token reaches too. `public`
3036/// is whether anyone may read it, and `exists` whether there is one.
3037///
3038/// A job's token and a deploy key reach their own repository only. Its
3039/// scopes decide the rest: `code:read` to read a private repository,
3040/// `code:write` to push, which a read-only deploy key never has. A deploy
3041/// key never makes a repository by pushing to an empty address.
3042pub(crate) fn git_token_refusal(
3043 access: &g1t_contracts::scopes::TokenAccess,
3044 repo: &str,
3045 source: Option<&str>,
3046 write: bool,
3047 public: bool,
3048 exists: bool,
3049) -> Option<String> {
3050 if let Some(refused) = g1t_contracts::scopes::decide_repo(access, repo)
3051 && !source.is_some_and(|source| access.reaches(source))
3052 {
3053 return Some(refused.reason.unwrap_or_default());
3054 }
3055 let decision = g1t_contracts::scopes::decide_git(access, write, public);
3056 if !decision.allowed {
3057 return Some(decision.reason.unwrap_or_default());
3058 }
3059 if access.deploy_key.is_some() && !exists {
3060 return Some(format!("This deploy key is for {repo}, which is not there any more."));
3061 }
3062 None
3063}
3064
3065/// The repository a push to a path that does not exist yet creates: private,
3066/// so nothing pushed by mistake is published. An owner makes it public on
3067/// purpose (`POST /repos/{owner}/{repo}/visibility`).
3068fn push_to_create(owner: &User, path: &RepoPath) -> CreateArgs {
3069 CreateArgs {
3070 owner: owner.clone(),
3071 namespace: path.namespace.clone(),
3072 name: path.name.clone(),
3073 description: None,
3074 is_private: true,
3075 import_url: None,
3076 import_token: None,
3077 }
3078}
3079
3080/// Whether `delete_branch` may remove `branch`: one of g1t's own
3081/// (`g1t-…`), or one whose tip the caller names, such as a dependency
3082/// update's branch after its pull request closed.
3083fn deletable_branch(branch: &str, head: Option<&str>) -> bool {
3084 !branch.is_empty() && (branch.starts_with(G1T_BRANCH_PREFIX) || head.is_some_and(|head| !head.is_empty()))
3085}
3086
3087#[cfg(test)]
3088mod delete_branch_tests {
3089 use super::deletable_branch;
3090
3091 #[test]
3092 fn only_g1t_branches_or_a_named_tip_are_deleted() {
3093 assert!(deletable_branch("g1t-queue-12", None));
3094 assert!(!deletable_branch("g1t/security/sharp-0.35.5", None));
3095 assert!(deletable_branch("g1t/security/sharp-0.35.5", Some("abc123")));
3096 assert!(!deletable_branch("feature", Some("")));
3097 assert!(!deletable_branch("", Some("abc123")));
3098 }
3099}
3100
3101#[cfg(test)]
3102mod announced_refs_tests {
3103 use super::*;
3104
3105 fn pushed(git_ref: String) -> git_http::Pushed {
3106 git_http::Pushed { git_ref, before: None, after: "abc".into() }
3107 }
3108
3109 fn refs(announced: Vec<&git_http::Pushed>) -> Vec<&str> {
3110 announced.into_iter().map(|pushed| pushed.git_ref.as_str()).collect()
3111 }
3112
3113 #[test]
3114 fn a_few_tags_are_announced_and_many_are_not() {
3115 let few: Vec<_> = (1..=3).map(|n| pushed(format!("refs/tags/v{n}"))).chain([pushed("refs/heads/main".into())]).collect();
3116 assert_eq!(refs(announced_refs(&few, "main")), ["refs/heads/main", "refs/tags/v1", "refs/tags/v2", "refs/tags/v3"]);
3117 let many: Vec<_> = (1..=10_000).map(|n| pushed(format!("refs/tags/v{n}"))).chain([pushed("refs/heads/main".into())]).collect();
3118 assert_eq!(refs(announced_refs(&many, "main")), ["refs/heads/main"]);
3119 }
3120
3121 #[test]
3122 fn past_the_branch_cap_only_the_default_branch_is_announced() {
3123 let at_cap: Vec<_> = (0..MAX_PUSH_BRANCH_EVENTS).map(|n| pushed(format!("refs/heads/b{n}"))).collect();
3124 assert_eq!(announced_refs(&at_cap, "main").len(), MAX_PUSH_BRANCH_EVENTS);
3125 let over: Vec<_> = (0..=MAX_PUSH_BRANCH_EVENTS)
3126 .map(|n| pushed(format!("refs/heads/b{n}")))
3127 .chain([pushed("refs/heads/main".into())])
3128 .collect();
3129 assert_eq!(refs(announced_refs(&over, "main")), ["refs/heads/main"]);
3130 assert!(announced_refs(&over, "trunk").is_empty());
3131 }
3132}
3133
3134#[cfg(test)]
3135mod push_to_create_tests {
3136 use super::*;
3137
3138 #[test]
3139 fn a_pushed_repository_starts_private() {
3140 let owner: User = serde_json::from_value(serde_json::json!({ "id": "usr_1", "username": "ada" })).unwrap();
3141 let args = push_to_create(&owner, &RepoPath { namespace: "acme".into(), name: "site".into() });
3142 assert!(args.is_private);
3143 assert_eq!((args.namespace.as_str(), args.name.as_str()), ("acme", "site"));
3144 }
3145}
3146
3147#[cfg(test)]
3148mod deploy_key_git_tests {
3149 use super::*;
3150 use g1t_contracts::deploy_keys;
3151
3152 fn key(read_only: bool) -> User {
3153 deploy_keys::principal("wsp_acme", "acme", deploy_keys::access("dk_1", "CI", "acme/rocket", read_only))
3154 }
3155
3156 fn rocket(private: bool) -> Repo {
3157 serde_json::from_value(serde_json::json!({
3158 "id": "rep_rocket",
3159 "namespace": "acme",
3160 "name": "rocket",
3161 "description": null,
3162 "isPrivate": private,
3163 "ownerId": "usr_owner",
3164 "defaultBranch": "main",
3165 "forkOf": null,
3166 "protected": false,
3167 "createdAt": "",
3168 }))
3169 .unwrap()
3170 }
3171
3172 fn refusal(user: &User, repo: &str, write: bool, exists: bool) -> Option<String> {
3173 git_token_refusal(user.token.as_deref().unwrap(), repo, None, write, false, exists)
3174 }
3175
3176 #[test]
3177 fn a_read_only_deploy_key_clones_its_repository_and_never_pushes() {
3178 let user = key(true);
3179 assert_eq!(refusal(&user, "acme/rocket", false, true), None);
3180 assert!(refusal(&user, "acme/rocket", true, true).unwrap().contains("read-only"));
3181 // Its role is a workspace token's: it reads a private repository.
3182 assert!(registry::can_read(&rocket(true), &Some(user)));
3183 }
3184
3185 #[test]
3186 fn a_deploy_key_with_write_access_pushes_to_its_repository() {
3187 let user = key(false);
3188 assert_eq!(refusal(&user, "acme/rocket", true, true), None);
3189 assert!(registry::can_write(&rocket(true), &Some(user)));
3190 }
3191
3192 #[test]
3193 fn a_deploy_key_reaches_no_other_repository() {
3194 let user = key(false);
3195 for other in ["acme/booster", "other/rocket"] {
3196 for write in [false, true] {
3197 let why = refusal(&user, other, write, true).expect(other);
3198 assert!(why.contains("deploy key is for acme/rocket"), "{why}");
3199 }
3200 }
3201 // Not even a pull request's working copy of another repository.
3202 let token = user.token.as_deref().unwrap();
3203 assert!(git_token_refusal(token, "pulls/pr_1", Some("acme/booster"), false, false, true).is_some());
3204 }
3205
3206 #[test]
3207 fn a_deploy_key_never_creates_a_repository() {
3208 let why = refusal(&key(false), "acme/rocket", true, false).unwrap();
3209 assert!(why.contains("not there"), "{why}");
3210 }
3211}