pr_01m47d15m3e54sn21z27rpy5n9/services/repos/src/lib.rs

988 lines36,831 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Rust repos service with shipping; pull requests kept in the model1//! 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
Agents as a team: lifecycle, merge queue, billing and a new shell8mod blame;
Diffs on attempts; hosted agent presented as the g1t agent9mod diff;
Rust repos service with shipping; pull requests kept in the model10mod git_http;
Agents as a team: lifecycle, merge queue, billing and a new shell11mod import;
Rust repos service with shipping; pull requests kept in the model12mod land;
Pull requests from branches13mod refs;
Rust repos service with shipping; pull requests kept in the model14mod registry;
15mod store;
16
Events service in Rust, with RFC 3339 times and accurate push events17use g1t_contracts::events::{GitPush, NewEvent, Publish, RepoCreated, RepoForked};
Rust repos service with shipping; pull requests kept in the model18use g1t_contracts::repos::*;
RFC 3339 timestamps in identity and repos19use g1t_contracts::time::rfc3339;
Agents as a team: lifecycle, merge queue, billing and a new shell20use g1t_contracts::{FailureCode, Outcome, PrincipalKind, User, Viewer, is_valid_repo_name, new_id};
Events service in Rust, with RFC 3339 times and accurate push events21use g1t_kit::{args, now_ms, reply, rpc_method};
Diffs on attempts; hosted agent presented as the g1t agent22use std::collections::{HashMap, HashSet, VecDeque};
Rust repos service with shipping; pull requests kept in the model23
24use serde::Serialize;
Events service in Rust, with RFC 3339 times and accurate push events25use worker::{Context, Env, Fetcher, Request, Response, Result, event};
Rust repos service with shipping; pull requests kept in the model26
27use registry::{Registry, can_read, can_write, store_key};
28use store::{ArtifactsStore, GitRepo, GitStore, Scope};
29
Issues and pull requests replace intents and attempts30/// Namespace that holds every pull request's fork: `pulls/<pull id>`.
31const PULLS_NAMESPACE: &str = "pulls";
Rust repos service with shipping; pull requests kept in the model32const MAX_TEXT_BYTES: usize = 512 * 1024;
Issues and pull requests replace intents and attempts33/// How far back a pull request may have forked and still be landed.
Rust repos service with shipping; pull requests kept in the model34const MAX_ANCESTRY: u32 = 1000;
Agents as a team: lifecycle, merge queue, billing and a new shell35const MAX_DESCRIPTION_CHARS: usize = 200;
Rust repos service with shipping; pull requests kept in the model36const SOURCE: &str = "repos";
37const UNVERIFIED: &str = "Confirm your email address first. Check your inbox, or resend the link from the banner on g1t.sh.";
38
39fn not_found<T>() -> Outcome<T> {
40 Outcome::fail(FailureCode::NotFound, "Repository not found.")
41}
42
43/// Decoded text, or `None` when the file is too large or looks binary.
Agents as a team: lifecycle, merge queue, billing and a new shell44/// Whether a ref is a full commit hash rather than a branch name.
45fn is_commit_hash(git_ref: &str) -> bool {
46 git_ref.len() == 40 && git_ref.bytes().all(|b| b.is_ascii_hexdigit())
47}
48
Rust repos service with shipping; pull requests kept in the model49fn text_of(bytes: Vec<u8>) -> Option<String> {
50 if bytes.len() > MAX_TEXT_BYTES || bytes.contains(&0) {
51 return None;
52 }
53 Some(String::from_utf8_lossy(&bytes).into_owned())
54}
55
56fn is_readme(name: &str) -> bool {
57 matches!(
58 name.to_lowercase().as_str(),
59 "readme" | "readme.md" | "readme.markdown" | "readme.txt"
60 )
61}
62
63/// Whether `ancestor` is reachable from the newest commit in `history`.
64///
65/// `history` is the first-parent chain, which is all the store lists; a fork
66/// that merged the target branch in has the target's head on a second
67/// parent, so the walk follows every parent.
68async fn descends_from<R: GitRepo>(repo: &R, history: &[Commit], ancestor: &str) -> Result<bool> {
69 let known: HashMap<&str, &[String]> = history
70 .iter()
71 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
72 .collect();
73 let mut seen = HashSet::new();
74 let mut queue: Vec<String> = history
75 .first()
76 .map(|c| c.hash.clone())
77 .into_iter()
78 .collect();
79 while let Some(hash) = queue.pop() {
80 if hash == ancestor {
81 return Ok(true);
82 }
83 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
84 continue;
85 }
86 match known.get(hash.as_str()) {
87 Some(parents) => queue.extend(parents.iter().cloned()),
88 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
89 }
90 }
91 Ok(false)
92}
93
Diffs on attempts; hosted agent presented as the g1t agent94/// The commit closest to the newest in `history` that is also in `shared`:
95/// where a fork and the repository it came from last agreed.
96async fn nearest_ancestor_in<R: GitRepo>(
97 repo: &R,
98 history: &[Commit],
99 shared: &HashSet<String>,
100) -> Result<Option<String>> {
101 let known: HashMap<&str, &[String]> = history
102 .iter()
103 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
104 .collect();
105 let mut seen = HashSet::new();
106 let mut queue: VecDeque<String> = history
107 .first()
108 .map(|c| c.hash.clone())
109 .into_iter()
110 .collect();
111 while let Some(hash) = queue.pop_front() {
112 if shared.contains(&hash) {
113 return Ok(Some(hash));
114 }
115 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
116 continue;
117 }
118 match known.get(hash.as_str()) {
119 Some(parents) => queue.extend(parents.iter().cloned()),
120 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
121 }
122 }
123 Ok(None)
124}
125
Rust repos service with shipping; pull requests kept in the model126struct Repos<S: GitStore> {
127 registry: Registry,
128 store: S,
Events service in Rust, with RFC 3339 times and accurate push events129 events: Fetcher,
Rust repos service with shipping; pull requests kept in the model130}
131
132impl<S: GitStore> Repos<S> {
133 async fn publish<T: Serialize>(&self, event: NewEvent<T>) -> Result<()> {
Events service in Rust, with RFC 3339 times and accurate push events134 g1t_kit::call(
135 &self.events,
136 "publish",
137 &Publish {
138 events: vec![event],
139 },
140 )
141 .await
Rust repos service with shipping; pull requests kept in the model142 }
143
Members can read a private repository's pull request forks144 /// Whether the viewer may read `repo`. A pull request's fork of a
145 /// private repository can be read by everyone who can read that
146 /// repository, so its members can review and check out the change, as
147 /// well as by whoever opened the pull request.
148 async fn may_read(&self, repo: &Repo, viewer: &Viewer) -> Result<bool> {
149 if can_read(repo, viewer) {
150 return Ok(true);
151 }
152 let Some(source_id) = &repo.fork_of else {
153 return Ok(false);
154 };
Rust repos service with shipping; pull requests kept in the model155 Ok(self
156 .registry
Members can read a private repository's pull request forks157 .by_id(source_id)
Rust repos service with shipping; pull requests kept in the model158 .await?
Members can read a private repository's pull request forks159 .is_some_and(|source| can_read(&source, viewer)))
Rust repos service with shipping; pull requests kept in the model160 }
161
Members can read a private repository's pull request forks162 /// `repo`, if there is one and the viewer may read it.
163 async fn visible(&self, repo: Option<Repo>, viewer: &Viewer) -> Result<Option<Repo>> {
164 Ok(match repo {
165 Some(repo) if self.may_read(&repo, viewer).await? => Some(repo),
166 _ => None,
167 })
168 }
169
170 /// Resolves a repo the viewer may read; private repos look missing.
171 async fn readable(&self, path: &RepoPath, viewer: &Viewer) -> Result<Option<Repo>> {
172 self.visible(self.registry.by_path(path).await?, viewer)
173 .await
174 }
175
Rust repos service with shipping; pull requests kept in the model176 async fn get(&self, a: GetArgs) -> Result<Outcome<Repo>> {
177 Ok(self
178 .readable(&a.path, &a.viewer)
179 .await?
180 .map_or_else(not_found, Outcome::Ok))
181 }
182
183 async fn get_by_id(&self, a: GetByIdArgs) -> Result<Outcome<Repo>> {
184 Ok(self
Members can read a private repository's pull request forks185 .visible(self.registry.by_id(&a.id).await?, &a.viewer)
Rust repos service with shipping; pull requests kept in the model186 .await?
187 .map_or_else(not_found, Outcome::Ok))
188 }
189
Agents as a team: lifecycle, merge queue, billing and a new shell190 async fn update(&self, a: UpdateArgs) -> Result<Outcome<Repo>> {
191 let viewer = Some(a.actor.clone());
192 let Some(repo) = self.readable(&a.path, &viewer).await? else {
193 return Ok(not_found());
194 };
195 if repo.fork_of.is_some() || !can_write(&repo, &viewer) {
196 return Ok(Outcome::fail(
197 FailureCode::Forbidden,
198 "Only members of the repository's workspace can change its settings.",
199 ));
200 }
201 if !a.actor.verified {
202 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
203 }
204 let description = match a.description {
205 Some(text) => Some(
206 text.trim()
207 .chars()
208 .take(MAX_DESCRIPTION_CHARS)
209 .collect::<String>(),
210 )
211 .filter(|text| !text.is_empty()),
212 None => repo.description.clone(),
213 };
214 let is_private = a.is_private.unwrap_or(repo.is_private);
215 let protected = a.protected.unwrap_or(repo.protected);
216 self.registry
217 .update(&repo.id, description.as_deref(), is_private, protected)
218 .await?;
219 Ok(Outcome::Ok(Repo {
220 description,
221 is_private,
222 protected,
223 ..repo
224 }))
225 }
226
Rust repos service with shipping; pull requests kept in the model227 async fn create(&self, a: CreateArgs) -> Result<Outcome<Repo>> {
228 if !a.owner.verified {
229 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
230 }
231 let name = a.name.trim().to_lowercase();
232 if !is_valid_repo_name(&name) {
233 return Ok(Outcome::fail(
234 FailureCode::Invalid,
235 "Use letters, digits, dots, hyphens and underscores only.",
236 ));
237 }
Workspaces own repositories238 let namespace = a.namespace.trim().to_lowercase();
239 if namespace.is_empty() {
Rust repos service with shipping; pull requests kept in the model240 return Ok(Outcome::fail(
241 FailureCode::Invalid,
Workspaces own repositories242 "Say which workspace to create the repository in.",
243 ));
244 }
245 if !a.owner.is_member(&namespace) {
246 return Ok(Outcome::fail(
247 FailureCode::Forbidden,
248 "You are not a member of that workspace.",
Rust repos service with shipping; pull requests kept in the model249 ));
250 }
Workspaces own repositories251 let path = RepoPath { namespace, name };
Rust repos service with shipping; pull requests kept in the model252 if self.registry.by_path(&path).await?.is_some() {
253 return Ok(Outcome::fail(
254 FailureCode::Conflict,
Workspaces own repositories255 "That workspace already has a repository with that name.",
Rust repos service with shipping; pull requests kept in the model256 ));
257 }
Agents as a team: lifecycle, merge queue, billing and a new shell258 // An import is fetched before anything is created, so that an
259 // address that does not work leaves nothing behind.
260 let mut imported = None;
261 if let Some(url) = a
262 .import_url
263 .as_deref()
264 .map(str::trim)
265 .filter(|url| !url.is_empty())
266 {
267 let Some(url) = import::clean_url(url) else {
268 return Ok(Outcome::fail(
269 FailureCode::Invalid,
270 "Give the https address of a public repository, such as https://github.com/owner/repo.",
271 ));
272 };
273 let remote = match import::discover(&url).await? {
274 Ok(remote) => remote,
275 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
276 };
277 let pack = match import::fetch(&url, &remote.head).await? {
278 Ok(pack) => pack,
279 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
280 };
281 imported = Some((remote, pack));
282 }
Rust repos service with shipping; pull requests kept in the model283 let now = now_ms();
284 let repo = Repo {
285 id: new_id("rep", now),
286 namespace: path.namespace,
287 name: path.name,
288 description: a
289 .description
290 .map(|text| text.trim().to_owned())
291 .filter(|text| !text.is_empty()),
292 is_private: a.is_private,
293 owner_id: a.owner.id.clone(),
Agents as a team: lifecycle, merge queue, billing and a new shell294 default_branch: imported
295 .as_ref()
296 .map_or_else(|| "main".to_owned(), |(remote, _)| remote.branch.clone()),
Rust repos service with shipping; pull requests kept in the model297 fork_of: None,
Agents as a team: lifecycle, merge queue, billing and a new shell298 protected: false,
RFC 3339 timestamps in identity and repos299 created_at: rfc3339(now),
Rust repos service with shipping; pull requests kept in the model300 };
301 self.store
302 .create(
303 &store_key(&repo),
304 repo.description.as_deref(),
305 &repo.default_branch,
306 )
307 .await?;
308 self.registry.insert(&repo).await?;
Agents as a team: lifecycle, merge queue, billing and a new shell309 let mut pushed = None;
310 if let Some((remote, pack)) = imported {
311 let access = self
312 .store
313 .open(&store_key(&repo))
314 .await?
315 .access(Scope::Write)
316 .await?;
317 let stored =
318 land::push_pack(&access, &repo.default_branch, None, &remote.head, pack).await?;
319 if let Err(reason) = stored {
320 self.registry.remove(&repo.id).await?;
321 return Ok(Outcome::fail(
322 FailureCode::Invalid,
323 format!("The repository could not be stored: {reason}"),
324 ));
325 }
326 pushed = Some(remote.head);
327 }
Rust repos service with shipping; pull requests kept in the model328 self.publish(NewEvent {
329 kind: "repo.created",
330 source: SOURCE,
331 repo_id: Some(repo.id.clone()),
332 actor: Some(a.owner.id),
333 data: RepoCreated {
334 repo_id: repo.id.clone(),
335 namespace: repo.namespace.clone(),
336 name: repo.name.clone(),
337 is_private: repo.is_private,
338 },
339 })
340 .await?;
Agents as a team: lifecycle, merge queue, billing and a new shell341 if let Some(head) = pushed {
GitHub Actions on g1t, part one: reading workflows342 self.publish_push(
343 &repo,
344 &format!("refs/heads/{}", repo.default_branch),
345 None,
346 &head,
347 None,
348 )
Agents as a team: lifecycle, merge queue, billing and a new shell349 .await?;
350 }
Rust repos service with shipping; pull requests kept in the model351 Ok(Outcome::Ok(repo))
352 }
353
354 async fn tree(&self, a: TreeArgs) -> Result<Outcome<TreeView>> {
355 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
356 return Ok(not_found());
357 };
358 let git = self.store.open(&store_key(&repo)).await?;
359 let git_ref = a
360 .git_ref
361 .clone()
362 .unwrap_or_else(|| repo.default_branch.clone());
363
364 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
365 // An unknown ref is an error; a repo with no commits is just empty.
366 if a.git_ref.is_some() {
367 return Ok(Outcome::fail(
368 FailureCode::NotFound,
369 "No such branch, tag or commit.",
370 ));
371 }
372 return Ok(Outcome::Ok(TreeView {
373 repo,
374 git_ref,
375 path: a.tree_path,
376 head: None,
377 entries: Vec::new(),
378 readme: None,
379 }));
380 };
381
382 let no_directory = || Outcome::fail(FailureCode::NotFound, "No such directory.");
383 let mut entries = git.read_tree(&head.tree_hash).await?;
384 for segment in a.tree_path.split('/').filter(|segment| !segment.is_empty()) {
385 let next = entries.as_ref().and_then(|entries| {
386 entries
387 .iter()
388 .find(|entry| entry.name == segment && entry.kind == EntryKind::Tree)
389 });
390 let Some(next) = next else {
391 return Ok(no_directory());
392 };
393 entries = git.read_tree(&next.hash).await?;
394 }
395 let Some(mut entries) = entries else {
396 return Ok(no_directory());
397 };
398 // Directories first, then by name.
399 entries.sort_by(|a, b| {
400 (b.kind == EntryKind::Tree)
401 .cmp(&(a.kind == EntryKind::Tree))
402 .then_with(|| a.name.cmp(&b.name))
403 });
404
405 let readme_entry = entries
406 .iter()
407 .find(|entry| entry.kind == EntryKind::Blob && is_readme(&entry.name));
408 let readme = match readme_entry {
409 Some(entry) => git.read_blob(&entry.hash).await?.map(|bytes| Readme {
410 name: entry.name.clone(),
411 text: text_of(bytes),
412 }),
413 None => None,
414 };
415 Ok(Outcome::Ok(TreeView {
416 repo,
417 git_ref,
418 path: a.tree_path,
419 head: Some(head),
420 entries,
421 readme,
422 }))
423 }
424
425 async fn blob(&self, a: BlobArgs) -> Result<Outcome<BlobView>> {
426 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
427 return Ok(not_found());
428 };
429 let bytes = if a.file_path.is_empty() {
430 None
431 } else {
432 let git = self.store.open(&store_key(&repo)).await?;
433 git.read_file(&a.git_ref, &a.file_path).await?
434 };
435 let Some(bytes) = bytes else {
436 return Ok(Outcome::fail(FailureCode::NotFound, "No such file."));
437 };
438 Ok(Outcome::Ok(BlobView {
439 repo,
440 git_ref: a.git_ref,
441 path: a.file_path,
442 size: bytes.len() as u64,
443 text: text_of(bytes),
444 }))
445 }
446
Agents as a team: lifecycle, merge queue, billing and a new shell447 async fn blame(&self, a: BlameArgs) -> Result<Outcome<Blame>> {
448 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
449 return Ok(not_found());
450 };
451 let git = self.store.open(&store_key(&repo)).await?;
452 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
453 Ok(match blame::blame(&git, &git_ref, &a.file_path).await? {
454 Some(blame) => Outcome::Ok(blame),
455 None => not_found(),
456 })
457 }
458
Rust repos service with shipping; pull requests kept in the model459 async fn log(&self, a: LogArgs) -> Result<Outcome<Vec<Commit>>> {
460 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
461 return Ok(not_found());
462 };
463 let git = self.store.open(&store_key(&repo)).await?;
464 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
465 Ok(Outcome::Ok(git.log(&git_ref, a.limit).await?))
466 }
467
Pull requests from branches468 /// The repository's branches, default branch first.
469 async fn branches(&self, a: BranchesArgs) -> Result<Outcome<Vec<Branch>>> {
470 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
471 return Ok(not_found());
472 };
473 let mut branches = self.store.open(&store_key(&repo)).await?.branches().await?;
474 branches.sort_by_key(|branch| branch.name != repo.default_branch);
475 Ok(Outcome::Ok(branches))
476 }
477
Agents as a team: lifecycle, merge queue, billing and a new shell478 /// Whether a pull request's source lacks commits that the branch it
479 /// would merge into has.
480 async fn behind(&self, a: BehindArgs) -> Result<bool> {
481 let Some(source) = self.registry.by_id(&a.source_id).await? else {
482 return Ok(false);
483 };
484 let target = match &source.fork_of {
485 Some(id) => self.registry.by_id(id).await?,
486 None => Some(source.clone()),
487 };
488 let Some(target) = target else {
489 return Ok(false);
490 };
491 let branch = a.branch.unwrap_or_else(|| target.default_branch.clone());
492 let target_head = self
493 .store
494 .open(&store_key(&target))
495 .await?
496 .log(&target.default_branch, 1)
497 .await?
498 .into_iter()
499 .next()
500 .map(|commit| commit.hash);
501 let Some(target_head) = target_head else {
502 return Ok(false);
503 };
504 let source_git = self.store.open(&store_key(&source)).await?;
505 let history = source_git.log(&branch, MAX_ANCESTRY).await?;
506 if history.is_empty() {
507 return Ok(false);
508 }
509 Ok(!descends_from(&source_git, &history, &target_head).await?)
510 }
511
Pull requests from branches512 async fn head(&self, a: HeadArgs) -> Result<Option<String>> {
513 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
514 return Ok(None);
515 };
Workflows run when an agent's pull request is marked ready516 let branch = if a.branch.is_empty() { &repo.default_branch } else { &a.branch };
Pull requests from branches517 let git = self.store.open(&store_key(&repo)).await?;
518 Ok(git
Workflows run when an agent's pull request is marked ready519 .log(branch, 1)
Pull requests from branches520 .await?
521 .into_iter()
522 .next()
523 .map(|commit| commit.hash))
524 }
525
Merge queue: tested states are deleted once their entry leaves526 async fn delete_branch(&self, a: DeleteBranchArgs) -> Result<Outcome<bool>> {
527 if !a.branch.starts_with(G1T_BRANCH_PREFIX) {
528 return Ok(Outcome::fail(
529 FailureCode::Forbidden,
530 "Only branches g1t made for itself can be deleted this way.",
531 ));
532 }
533 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
534 return Ok(not_found());
535 };
536 let git = self.store.open(&store_key(&repo)).await?;
537 let Some(old) = git
538 .branches()
539 .await?
540 .into_iter()
541 .find(|branch| branch.name == a.branch)
542 .map(|branch| branch.hash)
543 else {
544 return Ok(Outcome::Ok(false));
545 };
546 let access = git.access(Scope::Write).await?;
547 if let Err(reason) = land::delete_ref(&access, &a.branch, &old).await? {
548 return Ok(Outcome::fail(
549 FailureCode::Conflict,
550 format!("{} could not be deleted: {reason}", a.branch),
551 ));
552 }
553 Ok(Outcome::Ok(true))
554 }
555
Issues and pull requests replace intents and attempts556 async fn fork_for_pull(&self, a: ForkArgs) -> Result<Outcome<Repo>> {
Rust repos service with shipping; pull requests kept in the model557 let viewer = Some(a.actor.clone());
558 let Some(source) = self
559 .registry
560 .by_id(&a.source_id)
561 .await?
562 .filter(|repo| can_read(repo, &viewer))
563 else {
564 return Ok(not_found());
565 };
566 let now = now_ms();
567 let fork = Repo {
568 id: new_id("rep", now),
Issues and pull requests replace intents and attempts569 namespace: PULLS_NAMESPACE.to_owned(),
570 name: a.pull_id.clone(),
Rust repos service with shipping; pull requests kept in the model571 description: None,
572 // A fork is exactly as visible as the repo it came from.
573 is_private: source.is_private,
574 owner_id: a.actor.id.clone(),
575 default_branch: source.default_branch.clone(),
576 fork_of: Some(source.id.clone()),
Agents as a team: lifecycle, merge queue, billing and a new shell577 protected: false,
RFC 3339 timestamps in identity and repos578 created_at: rfc3339(now),
Rust repos service with shipping; pull requests kept in the model579 };
580 self.store
581 .open(&store_key(&source))
582 .await?
583 .fork(&store_key(&fork))
584 .await?;
585 self.registry.insert(&fork).await?;
586 self.publish(NewEvent {
587 kind: "repo.forked",
588 source: SOURCE,
589 repo_id: Some(source.id.clone()),
590 actor: Some(a.actor.id),
591 data: RepoForked {
592 repo_id: fork.id.clone(),
593 source_repo_id: source.id,
Issues and pull requests replace intents and attempts594 pull_id: a.pull_id,
Rust repos service with shipping; pull requests kept in the model595 },
596 })
597 .await?;
598 Ok(Outcome::Ok(fork))
599 }
600
601 async fn git_access(&self, a: GitAccessArgs) -> Result<Outcome<GitAccess>> {
602 let write = a.service == GitService::ReceivePack;
603 // Anonymous callers are asked to authenticate whether or not the repo
604 // exists, so private repos cannot be told apart from missing ones.
605 let denied = || match &a.viewer {
606 Some(_) => not_found(),
607 None => Outcome::fail(FailureCode::Unauthenticated, "Authentication required."),
608 };
Agents as a team: lifecycle, merge queue, billing and a new shell609 // An agent's token works through the API only: its sandbox has its
610 // own way to push, to its own pull request.
611 if a.viewer.as_ref().is_some_and(|user| user.kind == PrincipalKind::Agent) {
612 return Ok(Outcome::fail(
613 FailureCode::Forbidden,
614 "A g1t agent's token cannot be used with git.",
615 ));
616 }
Rust repos service with shipping; pull requests kept in the model617 if let (true, Some(user)) = (write, &a.viewer)
618 && !user.verified
619 {
620 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
621 }
622
623 let repo = match self.registry.by_path(&a.path).await? {
624 Some(repo) => {
625 let allowed = if write {
626 can_write(&repo, &a.viewer)
627 } else {
Members can read a private repository's pull request forks628 self.may_read(&repo, &a.viewer).await?
Rust repos service with shipping; pull requests kept in the model629 };
630 if !allowed {
631 return Ok(denied());
632 }
633 repo
634 }
635 None => {
Workspaces own repositories636 // Push to create, in a workspace the pusher belongs to.
Rust repos service with shipping; pull requests kept in the model637 let owner = a
638 .viewer
639 .as_ref()
Workspaces own repositories640 .filter(|user| write && user.is_member(&a.path.namespace.to_lowercase()));
Rust repos service with shipping; pull requests kept in the model641 let Some(owner) = owner else {
642 return Ok(denied());
643 };
644 let created = self
645 .create(CreateArgs {
646 owner: owner.clone(),
Workspaces own repositories647 namespace: a.path.namespace.clone(),
Rust repos service with shipping; pull requests kept in the model648 name: a.path.name.clone(),
649 description: None,
650 is_private: false,
Agents as a team: lifecycle, merge queue, billing and a new shell651 import_url: None,
Rust repos service with shipping; pull requests kept in the model652 })
653 .await?;
654 match created {
655 Outcome::Ok(repo) => repo,
656 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
657 }
658 }
659 };
660 let git = self.store.open(&store_key(&repo)).await?;
661 let scope = if write { Scope::Write } else { Scope::Read };
662 Ok(Outcome::Ok(git.access(scope).await?))
663 }
664
665 async fn land(&self, a: LandArgs) -> Result<Outcome<Landed>> {
666 let actor: Viewer = Some(a.actor.clone());
Pull requests from branches667 let Some(source) = self.registry.by_id(&a.source_id).await? else {
Rust repos service with shipping; pull requests kept in the model668 return Ok(not_found());
669 };
Pull requests from branches670 // A fork lands on the repository it came from; a branch on its own.
671 let target = match &source.fork_of {
Rust repos service with shipping; pull requests kept in the model672 Some(id) => self.registry.by_id(id).await?,
Pull requests from branches673 None => Some(source.clone()),
Rust repos service with shipping; pull requests kept in the model674 };
675 let Some(target) = target.filter(|repo| can_read(repo, &actor)) else {
676 return Ok(not_found());
677 };
678 if !can_write(&target, &actor) {
679 return Ok(Outcome::fail(
680 FailureCode::Forbidden,
Issues and pull requests replace intents and attempts681 "Only members of the repository's workspace can merge a pull request.",
Rust repos service with shipping; pull requests kept in the model682 ));
683 }
684 if !a.actor.verified {
685 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
686 }
687
688 let branch = &target.default_branch;
Pull requests from branches689 let from_fork = source.id != target.id;
690 let source_branch = match a.branch {
691 Some(name) if !from_fork && name == *branch => {
692 return Ok(Outcome::fail(
693 FailureCode::Invalid,
694 format!("{branch} cannot be merged into itself."),
695 ));
696 }
697 Some(name) => name,
698 None if from_fork => branch.clone(),
699 None => {
700 return Ok(Outcome::fail(
701 FailureCode::Invalid,
702 "Say which branch to merge.",
703 ));
704 }
705 };
706
707 let source_git = self.store.open(&store_key(&source)).await?;
Rust repos service with shipping; pull requests kept in the model708 let target_git = self.store.open(&store_key(&target)).await?;
Pull requests from branches709 let history = source_git.log(&source_branch, MAX_ANCESTRY).await?;
Rust repos service with shipping; pull requests kept in the model710 let Some(new) = history.first().map(|commit| commit.hash.clone()) else {
711 return Ok(Outcome::fail(
712 FailureCode::Conflict,
Issues and pull requests replace intents and attempts713 "This pull request has no commits to merge.",
Rust repos service with shipping; pull requests kept in the model714 ));
715 };
716 let old = target_git
717 .log(branch, 1)
718 .await?
719 .into_iter()
720 .next()
721 .map(|commit| commit.hash);
722
723 if old.as_deref() == Some(new.as_str()) {
Diffs on attempts; hosted agent presented as the g1t agent724 return Ok(Outcome::Ok(Landed {
725 commit: new,
726 previous: None,
727 }));
Rust repos service with shipping; pull requests kept in the model728 }
729 // Moving the branch to a commit that does not descend from its
730 // current head would discard whatever landed in between.
731 if let Some(old) = &old
Pull requests from branches732 && !descends_from(&source_git, &history, old).await?
Rust repos service with shipping; pull requests kept in the model733 {
Pull requests from branches734 let remedy = if from_fork {
735 format!("Pull {branch} into the pull request's fork, push, and merge again.")
736 } else {
737 format!("Merge {branch} into {source_branch}, push, and merge again.")
738 };
Rust repos service with shipping; pull requests kept in the model739 return Ok(Outcome::fail(
740 FailureCode::Conflict,
Pull requests from branches741 format!("{branch} has moved since this pull request was opened. {remedy}"),
Rust repos service with shipping; pull requests kept in the model742 ));
743 }
744
Pull requests from branches745 // For a branch the objects are already in the target; sending them
746 // again is harmless and keeps one way of moving a ref.
747 let source_access = source_git.access(Scope::Read).await?;
Rust repos service with shipping; pull requests kept in the model748 let target_access = target_git.access(Scope::Write).await?;
749 let pushed =
750 land::fast_forward(&source_access, &target_access, branch, old.as_deref(), &new)
751 .await?;
752 if let Err(reason) = pushed {
Issues and pull requests replace intents and attempts753 // Most often another pull request landed between the check and the push.
Rust repos service with shipping; pull requests kept in the model754 return Ok(Outcome::fail(
755 FailureCode::Conflict,
756 format!("{branch} could not be updated: {reason}"),
757 ));
758 }
GitHub Actions on g1t, part one: reading workflows759 self.publish_push(
760 &target,
761 &format!("refs/heads/{branch}"),
762 old.as_deref(),
763 &new,
764 Some(a.actor.id),
765 )
Events service in Rust, with RFC 3339 times and accurate push events766 .await?;
Diffs on attempts; hosted agent presented as the g1t agent767 Ok(Outcome::Ok(Landed {
768 commit: new,
769 previous: old,
770 }))
771 }
772
773 async fn compare(&self, a: CompareArgs) -> Result<Outcome<Comparison>> {
774 let Some(repo) = self
Members can read a private repository's pull request forks775 .visible(self.registry.by_id(&a.repo_id).await?, &a.viewer)
Diffs on attempts; hosted agent presented as the g1t agent776 .await?
777 else {
778 return Ok(not_found());
779 };
780 let git = self.store.open(&store_key(&repo)).await?;
Pull requests from branches781 let head_ref = a.head.as_deref().unwrap_or(&repo.default_branch);
Agents as a team: lifecycle, merge queue, billing and a new shell782 // The head's history is only searched when the base is worked out
783 // from another branch.
784 let depth = if a.base.is_some() || is_commit_hash(head_ref) { 1 } else { MAX_ANCESTRY };
785 let history = git.log(head_ref, depth).await?;
Diffs on attempts; hosted agent presented as the g1t agent786 let Some(head) = history.first() else {
787 return Ok(Outcome::fail(
788 FailureCode::Conflict,
Pull requests from branches789 "There are no commits to compare.",
Diffs on attempts; hosted agent presented as the g1t agent790 ));
791 };
792
Pull requests from branches793 // Where the head's history meets the default branch of `against`.
794 let shared_with = async |against: &Repo| -> Result<Option<String>> {
795 let against_git = self.store.open(&store_key(against)).await?;
796 let shared: HashSet<String> = against_git
797 .log(&against.default_branch, MAX_ANCESTRY)
798 .await?
799 .into_iter()
800 .map(|commit| commit.hash)
801 .collect();
802 nearest_ancestor_in(&git, &history, &shared).await
803 };
Diffs on attempts; hosted agent presented as the g1t agent804 let base = match (a.base, &repo.fork_of) {
805 (Some(base), _) => Some(base),
806 // A fork is compared with the last commit it shares with the
807 // repository it came from.
808 (None, Some(target_id)) => match self.registry.by_id(target_id).await? {
Pull requests from branches809 Some(target) => shared_with(&target).await?,
Diffs on attempts; hosted agent presented as the g1t agent810 None => None,
811 },
Pull requests from branches812 // A branch, with the point where it left the default branch.
Agents as a team: lifecycle, merge queue, billing and a new shell813 // A single commit, with its first parent.
814 (None, None) if is_commit_hash(head_ref) => head.parents.first().cloned(),
Pull requests from branches815 (None, None) if head_ref != repo.default_branch => shared_with(&repo).await?,
Diffs on attempts; hosted agent presented as the g1t agent816 (None, None) => head.parents.first().cloned(),
817 };
818 let base_tree = match &base {
819 Some(base) => git
820 .log(base, 1)
821 .await?
822 .into_iter()
823 .next()
824 .map(|commit| commit.tree_hash),
825 None => None,
826 };
827 let (files, truncated) =
828 diff::compare_trees(&git, base_tree.as_deref(), &head.tree_hash).await?;
829 Ok(Outcome::Ok(Comparison {
830 base,
831 head: head.hash.clone(),
832 files,
833 truncated,
834 }))
Rust repos service with shipping; pull requests kept in the model835 }
836
GitHub Actions on g1t, part one: reading workflows837 /// Reports that `git_ref` of `repo` (a full ref) now points to `after`.
Events service in Rust, with RFC 3339 times and accurate push events838 async fn publish_push(
839 &self,
840 repo: &Repo,
GitHub Actions on g1t, part one: reading workflows841 git_ref: &str,
842 before: Option<&str>,
Events service in Rust, with RFC 3339 times and accurate push events843 after: &str,
844 actor: Option<String>,
845 ) -> Result<()> {
Rust repos service with shipping; pull requests kept in the model846 self.publish(NewEvent {
847 kind: "git.push",
848 source: SOURCE,
849 repo_id: Some(repo.id.clone()),
850 actor,
851 data: GitPush {
852 repo_id: repo.id.clone(),
GitHub Actions on g1t, part one: reading workflows853 git_ref: git_ref.to_owned(),
854 before: before.map(str::to_owned),
Rust repos service with shipping; pull requests kept in the model855 after: after.to_owned(),
GitHub Actions on g1t, part one: reading workflows856 default_branch: git_ref.strip_prefix("refs/heads/")
857 == Some(repo.default_branch.as_str()),
Rust repos service with shipping; pull requests kept in the model858 },
859 })
860 .await
861 }
862
863 /// Git over HTTPS.
864 async fn git_http(&self, request: Request, env: &Env) -> Result<Response> {
865 let Some(git) = git_http::parse(&request.url()?) else {
866 return Response::error("Not found", 404);
867 };
868 let viewer = git_http::viewer(&request, &env.service("IDENTITY")?).await?;
869 let access = self
870 .git_access(GitAccessArgs {
871 path: git.path.clone(),
872 viewer: viewer.clone(),
873 service: git.service,
874 })
875 .await?;
876 let access = match access {
877 Outcome::Ok(access) => access,
878 refused => return git_http::refuse(refused),
879 };
Agents as a team: lifecycle, merge queue, billing and a new shell880 // A protected default branch takes changes only from a merged pull
881 // request, which lands without going through here.
882 let protected = match self.registry.by_path(&git.path).await? {
883 Some(repo) if repo.protected && repo.fork_of.is_none() => Some(repo.default_branch),
884 _ => None,
885 };
886 let forwarded =
887 match git_http::forward(request, &git, &access, protected.as_deref()).await? {
888 git_http::Push::Forwarded(forwarded) => forwarded,
889 git_http::Push::Refused(response) => return Ok(response),
890 };
Rust repos service with shipping; pull requests kept in the model891
892 // Artifacts' own push notifications are per repository, which does
Events service in Rust, with RFC 3339 times and accurate push events893 // not fit a repo per pull request, so the front end reports pushes
894 // itself: one event for each branch that moved.
895 let accepted = forwarded.response.status_code() == 200 && !forwarded.pushed.is_empty();
896 if accepted && let Some(repo) = self.registry.by_path(&git.path).await? {
897 let stored = self.store.open(&store_key(&repo)).await?;
898 let actor = viewer.map(|user: User| user.id);
GitHub Actions on g1t, part one: reading workflows899 for pushed in &forwarded.pushed {
Events service in Rust, with RFC 3339 times and accurate push events900 // The store can refuse one ref and accept another, so each
GitHub Actions on g1t, part one: reading workflows901 // branch is checked against where it actually is. A tag the
902 // store cannot read back is taken as pushed.
903 let moved = match pushed.branch() {
904 Some(branch) => stored
905 .log(branch, 1)
906 .await?
907 .first()
908 .is_some_and(|commit| commit.hash == pushed.after),
909 None => stored.log(&pushed.git_ref, 1).await.map_or(true, |head| {
910 head.first().is_none_or(|commit| commit.hash == pushed.after)
911 }),
912 };
913 if moved {
914 self.publish_push(
915 &repo,
916 &pushed.git_ref,
917 pushed.before.as_deref(),
918 &pushed.after,
919 actor.clone(),
920 )
921 .await?;
Events service in Rust, with RFC 3339 times and accurate push events922 }
Rust repos service with shipping; pull requests kept in the model923 }
924 }
Events service in Rust, with RFC 3339 times and accurate push events925 Ok(forwarded.response)
Rust repos service with shipping; pull requests kept in the model926 }
927}
928
929#[event(fetch)]
930async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
931 let repos = Repos {
932 registry: Registry { db: env.d1("DB")? },
933 store: ArtifactsStore::new(&env)?,
Events service in Rust, with RFC 3339 times and accurate push events934 events: env.service("EVENTS")?,
Rust repos service with shipping; pull requests kept in the model935 };
936 let Some(method) = rpc_method(&request) else {
937 return repos.git_http(request, &env).await;
938 };
939 let body: serde_json::Value = request.json().await?;
940
941 match method.as_str() {
942 "get" => reply(&repos.get(args(body)?).await?),
943 "get_by_id" => reply(&repos.get_by_id(args(body)?).await?),
Automations: rules in .g1t/automations that act when something happens944 "path_by_id" => {
945 let a: PathByIdArgs = args(body)?;
946 reply(
947 &repos
948 .registry
949 .by_id(&a.id)
950 .await?
951 .filter(|repo| repo.fork_of.is_none())
952 .map(|repo| RepoPath {
953 namespace: repo.namespace,
954 name: repo.name,
955 }),
956 )
957 }
Rust repos service with shipping; pull requests kept in the model958 "list" => {
959 let a: ListArgs = args(body)?;
960 reply(
961 &repos
962 .registry
Workspaces own repositories963 .list(
964 &a.viewer,
965 a.query.as_deref(),
966 a.namespace.as_deref(),
967 a.member_only,
968 )
Rust repos service with shipping; pull requests kept in the model969 .await?,
970 )
971 }
972 "create" => reply(&repos.create(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell973 "update" => reply(&repos.update(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model974 "tree" => reply(&repos.tree(args(body)?).await?),
975 "blob" => reply(&repos.blob(args(body)?).await?),
976 "log" => reply(&repos.log(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell977 "blame" => reply(&repos.blame(args(body)?).await?),
Issues and pull requests replace intents and attempts978 "fork_for_pull" => reply(&repos.fork_for_pull(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model979 "git_access" => reply(&repos.git_access(args(body)?).await?),
Pull requests from branches980 "branches" => reply(&repos.branches(args(body)?).await?),
981 "head" => reply(&repos.head(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell982 "behind" => reply(&repos.behind(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model983 "land" => reply(&repos.land(args(body)?).await?),
Merge queue: tested states are deleted once their entry leaves984 "delete_branch" => reply(&repos.delete_branch(args(body)?).await?),
Diffs on attempts; hosted agent presented as the g1t agent985 "compare" => reply(&repos.compare(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model986 _ => Response::error("Unknown method", 404),
987 }
988}