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