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.

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