Skip to content

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

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