g1t/services/repos/src/lib.rs

741 lines27,188 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
Diffs on attempts; hosted agent presented as the g1t agent8mod diff;
Rust repos service with shipping; pull requests kept in the model9mod git_http;
10mod land;
Pull requests from branches11mod refs;
Rust repos service with shipping; pull requests kept in the model12mod registry;
13mod store;
14
Events service in Rust, with RFC 3339 times and accurate push events15use g1t_contracts::events::{GitPush, NewEvent, Publish, RepoCreated, RepoForked};
Rust repos service with shipping; pull requests kept in the model16use g1t_contracts::repos::*;
RFC 3339 timestamps in identity and repos17use g1t_contracts::time::rfc3339;
Workspaces own repositories18use g1t_contracts::{FailureCode, Outcome, User, Viewer, is_valid_repo_name, new_id};
Events service in Rust, with RFC 3339 times and accurate push events19use g1t_kit::{args, now_ms, reply, rpc_method};
Diffs on attempts; hosted agent presented as the g1t agent20use std::collections::{HashMap, HashSet, VecDeque};
Rust repos service with shipping; pull requests kept in the model21
22use serde::Serialize;
Events service in Rust, with RFC 3339 times and accurate push events23use worker::{Context, Env, Fetcher, Request, Response, Result, event};
Rust repos service with shipping; pull requests kept in the model24
25use registry::{Registry, can_read, can_write, store_key};
26use store::{ArtifactsStore, GitRepo, GitStore, Scope};
27
Issues and pull requests replace intents and attempts28/// Namespace that holds every pull request's fork: `pulls/<pull id>`.
29const PULLS_NAMESPACE: &str = "pulls";
Rust repos service with shipping; pull requests kept in the model30const MAX_TEXT_BYTES: usize = 512 * 1024;
Issues and pull requests replace intents and attempts31/// How far back a pull request may have forked and still be landed.
Rust repos service with shipping; pull requests kept in the model32const MAX_ANCESTRY: u32 = 1000;
33const SOURCE: &str = "repos";
34const UNVERIFIED: &str = "Confirm your email address first. Check your inbox, or resend the link from the banner on g1t.sh.";
35
36fn not_found<T>() -> Outcome<T> {
37 Outcome::fail(FailureCode::NotFound, "Repository not found.")
38}
39
40/// Decoded text, or `None` when the file is too large or looks binary.
41fn text_of(bytes: Vec<u8>) -> Option<String> {
42 if bytes.len() > MAX_TEXT_BYTES || bytes.contains(&0) {
43 return None;
44 }
45 Some(String::from_utf8_lossy(&bytes).into_owned())
46}
47
48fn is_readme(name: &str) -> bool {
49 matches!(
50 name.to_lowercase().as_str(),
51 "readme" | "readme.md" | "readme.markdown" | "readme.txt"
52 )
53}
54
55/// Whether `ancestor` is reachable from the newest commit in `history`.
56///
57/// `history` is the first-parent chain, which is all the store lists; a fork
58/// that merged the target branch in has the target's head on a second
59/// parent, so the walk follows every parent.
60async fn descends_from<R: GitRepo>(repo: &R, history: &[Commit], ancestor: &str) -> Result<bool> {
61 let known: HashMap<&str, &[String]> = history
62 .iter()
63 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
64 .collect();
65 let mut seen = HashSet::new();
66 let mut queue: Vec<String> = history
67 .first()
68 .map(|c| c.hash.clone())
69 .into_iter()
70 .collect();
71 while let Some(hash) = queue.pop() {
72 if hash == ancestor {
73 return Ok(true);
74 }
75 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
76 continue;
77 }
78 match known.get(hash.as_str()) {
79 Some(parents) => queue.extend(parents.iter().cloned()),
80 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
81 }
82 }
83 Ok(false)
84}
85
Diffs on attempts; hosted agent presented as the g1t agent86/// The commit closest to the newest in `history` that is also in `shared`:
87/// where a fork and the repository it came from last agreed.
88async fn nearest_ancestor_in<R: GitRepo>(
89 repo: &R,
90 history: &[Commit],
91 shared: &HashSet<String>,
92) -> Result<Option<String>> {
93 let known: HashMap<&str, &[String]> = history
94 .iter()
95 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
96 .collect();
97 let mut seen = HashSet::new();
98 let mut queue: VecDeque<String> = history
99 .first()
100 .map(|c| c.hash.clone())
101 .into_iter()
102 .collect();
103 while let Some(hash) = queue.pop_front() {
104 if shared.contains(&hash) {
105 return Ok(Some(hash));
106 }
107 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
108 continue;
109 }
110 match known.get(hash.as_str()) {
111 Some(parents) => queue.extend(parents.iter().cloned()),
112 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
113 }
114 }
115 Ok(None)
116}
117
Rust repos service with shipping; pull requests kept in the model118struct Repos<S: GitStore> {
119 registry: Registry,
120 store: S,
Events service in Rust, with RFC 3339 times and accurate push events121 events: Fetcher,
Rust repos service with shipping; pull requests kept in the model122}
123
124impl<S: GitStore> Repos<S> {
125 async fn publish<T: Serialize>(&self, event: NewEvent<T>) -> Result<()> {
Events service in Rust, with RFC 3339 times and accurate push events126 g1t_kit::call(
127 &self.events,
128 "publish",
129 &Publish {
130 events: vec![event],
131 },
132 )
133 .await
Rust repos service with shipping; pull requests kept in the model134 }
135
Members can read a private repository's pull request forks136 /// Whether the viewer may read `repo`. A pull request's fork of a
137 /// private repository can be read by everyone who can read that
138 /// repository, so its members can review and check out the change, as
139 /// well as by whoever opened the pull request.
140 async fn may_read(&self, repo: &Repo, viewer: &Viewer) -> Result<bool> {
141 if can_read(repo, viewer) {
142 return Ok(true);
143 }
144 let Some(source_id) = &repo.fork_of else {
145 return Ok(false);
146 };
Rust repos service with shipping; pull requests kept in the model147 Ok(self
148 .registry
Members can read a private repository's pull request forks149 .by_id(source_id)
Rust repos service with shipping; pull requests kept in the model150 .await?
Members can read a private repository's pull request forks151 .is_some_and(|source| can_read(&source, viewer)))
Rust repos service with shipping; pull requests kept in the model152 }
153
Members can read a private repository's pull request forks154 /// `repo`, if there is one and the viewer may read it.
155 async fn visible(&self, repo: Option<Repo>, viewer: &Viewer) -> Result<Option<Repo>> {
156 Ok(match repo {
157 Some(repo) if self.may_read(&repo, viewer).await? => Some(repo),
158 _ => None,
159 })
160 }
161
162 /// Resolves a repo the viewer may read; private repos look missing.
163 async fn readable(&self, path: &RepoPath, viewer: &Viewer) -> Result<Option<Repo>> {
164 self.visible(self.registry.by_path(path).await?, viewer)
165 .await
166 }
167
Rust repos service with shipping; pull requests kept in the model168 async fn get(&self, a: GetArgs) -> Result<Outcome<Repo>> {
169 Ok(self
170 .readable(&a.path, &a.viewer)
171 .await?
172 .map_or_else(not_found, Outcome::Ok))
173 }
174
175 async fn get_by_id(&self, a: GetByIdArgs) -> Result<Outcome<Repo>> {
176 Ok(self
Members can read a private repository's pull request forks177 .visible(self.registry.by_id(&a.id).await?, &a.viewer)
Rust repos service with shipping; pull requests kept in the model178 .await?
179 .map_or_else(not_found, Outcome::Ok))
180 }
181
182 async fn create(&self, a: CreateArgs) -> Result<Outcome<Repo>> {
183 if !a.owner.verified {
184 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
185 }
186 let name = a.name.trim().to_lowercase();
187 if !is_valid_repo_name(&name) {
188 return Ok(Outcome::fail(
189 FailureCode::Invalid,
190 "Use letters, digits, dots, hyphens and underscores only.",
191 ));
192 }
Workspaces own repositories193 let namespace = a.namespace.trim().to_lowercase();
194 if namespace.is_empty() {
Rust repos service with shipping; pull requests kept in the model195 return Ok(Outcome::fail(
196 FailureCode::Invalid,
Workspaces own repositories197 "Say which workspace to create the repository in.",
198 ));
199 }
200 if !a.owner.is_member(&namespace) {
201 return Ok(Outcome::fail(
202 FailureCode::Forbidden,
203 "You are not a member of that workspace.",
Rust repos service with shipping; pull requests kept in the model204 ));
205 }
Workspaces own repositories206 let path = RepoPath { namespace, name };
Rust repos service with shipping; pull requests kept in the model207 if self.registry.by_path(&path).await?.is_some() {
208 return Ok(Outcome::fail(
209 FailureCode::Conflict,
Workspaces own repositories210 "That workspace already has a repository with that name.",
Rust repos service with shipping; pull requests kept in the model211 ));
212 }
213 let now = now_ms();
214 let repo = Repo {
215 id: new_id("rep", now),
216 namespace: path.namespace,
217 name: path.name,
218 description: a
219 .description
220 .map(|text| text.trim().to_owned())
221 .filter(|text| !text.is_empty()),
222 is_private: a.is_private,
223 owner_id: a.owner.id.clone(),
224 default_branch: "main".to_owned(),
225 fork_of: None,
RFC 3339 timestamps in identity and repos226 created_at: rfc3339(now),
Rust repos service with shipping; pull requests kept in the model227 };
228 self.store
229 .create(
230 &store_key(&repo),
231 repo.description.as_deref(),
232 &repo.default_branch,
233 )
234 .await?;
235 self.registry.insert(&repo).await?;
236 self.publish(NewEvent {
237 kind: "repo.created",
238 source: SOURCE,
239 repo_id: Some(repo.id.clone()),
240 actor: Some(a.owner.id),
241 data: RepoCreated {
242 repo_id: repo.id.clone(),
243 namespace: repo.namespace.clone(),
244 name: repo.name.clone(),
245 is_private: repo.is_private,
246 },
247 })
248 .await?;
249 Ok(Outcome::Ok(repo))
250 }
251
252 async fn tree(&self, a: TreeArgs) -> Result<Outcome<TreeView>> {
253 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
254 return Ok(not_found());
255 };
256 let git = self.store.open(&store_key(&repo)).await?;
257 let git_ref = a
258 .git_ref
259 .clone()
260 .unwrap_or_else(|| repo.default_branch.clone());
261
262 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
263 // An unknown ref is an error; a repo with no commits is just empty.
264 if a.git_ref.is_some() {
265 return Ok(Outcome::fail(
266 FailureCode::NotFound,
267 "No such branch, tag or commit.",
268 ));
269 }
270 return Ok(Outcome::Ok(TreeView {
271 repo,
272 git_ref,
273 path: a.tree_path,
274 head: None,
275 entries: Vec::new(),
276 readme: None,
277 }));
278 };
279
280 let no_directory = || Outcome::fail(FailureCode::NotFound, "No such directory.");
281 let mut entries = git.read_tree(&head.tree_hash).await?;
282 for segment in a.tree_path.split('/').filter(|segment| !segment.is_empty()) {
283 let next = entries.as_ref().and_then(|entries| {
284 entries
285 .iter()
286 .find(|entry| entry.name == segment && entry.kind == EntryKind::Tree)
287 });
288 let Some(next) = next else {
289 return Ok(no_directory());
290 };
291 entries = git.read_tree(&next.hash).await?;
292 }
293 let Some(mut entries) = entries else {
294 return Ok(no_directory());
295 };
296 // Directories first, then by name.
297 entries.sort_by(|a, b| {
298 (b.kind == EntryKind::Tree)
299 .cmp(&(a.kind == EntryKind::Tree))
300 .then_with(|| a.name.cmp(&b.name))
301 });
302
303 let readme_entry = entries
304 .iter()
305 .find(|entry| entry.kind == EntryKind::Blob && is_readme(&entry.name));
306 let readme = match readme_entry {
307 Some(entry) => git.read_blob(&entry.hash).await?.map(|bytes| Readme {
308 name: entry.name.clone(),
309 text: text_of(bytes),
310 }),
311 None => None,
312 };
313 Ok(Outcome::Ok(TreeView {
314 repo,
315 git_ref,
316 path: a.tree_path,
317 head: Some(head),
318 entries,
319 readme,
320 }))
321 }
322
323 async fn blob(&self, a: BlobArgs) -> Result<Outcome<BlobView>> {
324 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
325 return Ok(not_found());
326 };
327 let bytes = if a.file_path.is_empty() {
328 None
329 } else {
330 let git = self.store.open(&store_key(&repo)).await?;
331 git.read_file(&a.git_ref, &a.file_path).await?
332 };
333 let Some(bytes) = bytes else {
334 return Ok(Outcome::fail(FailureCode::NotFound, "No such file."));
335 };
336 Ok(Outcome::Ok(BlobView {
337 repo,
338 git_ref: a.git_ref,
339 path: a.file_path,
340 size: bytes.len() as u64,
341 text: text_of(bytes),
342 }))
343 }
344
345 async fn log(&self, a: LogArgs) -> Result<Outcome<Vec<Commit>>> {
346 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
347 return Ok(not_found());
348 };
349 let git = self.store.open(&store_key(&repo)).await?;
350 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
351 Ok(Outcome::Ok(git.log(&git_ref, a.limit).await?))
352 }
353
Pull requests from branches354 /// The repository's branches, default branch first.
355 async fn branches(&self, a: BranchesArgs) -> Result<Outcome<Vec<Branch>>> {
356 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
357 return Ok(not_found());
358 };
359 let mut branches = self.store.open(&store_key(&repo)).await?.branches().await?;
360 branches.sort_by_key(|branch| branch.name != repo.default_branch);
361 Ok(Outcome::Ok(branches))
362 }
363
364 async fn head(&self, a: HeadArgs) -> Result<Option<String>> {
365 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
366 return Ok(None);
367 };
368 let git = self.store.open(&store_key(&repo)).await?;
369 Ok(git
370 .log(&a.branch, 1)
371 .await?
372 .into_iter()
373 .next()
374 .map(|commit| commit.hash))
375 }
376
Issues and pull requests replace intents and attempts377 async fn fork_for_pull(&self, a: ForkArgs) -> Result<Outcome<Repo>> {
Rust repos service with shipping; pull requests kept in the model378 let viewer = Some(a.actor.clone());
379 let Some(source) = self
380 .registry
381 .by_id(&a.source_id)
382 .await?
383 .filter(|repo| can_read(repo, &viewer))
384 else {
385 return Ok(not_found());
386 };
387 let now = now_ms();
388 let fork = Repo {
389 id: new_id("rep", now),
Issues and pull requests replace intents and attempts390 namespace: PULLS_NAMESPACE.to_owned(),
391 name: a.pull_id.clone(),
Rust repos service with shipping; pull requests kept in the model392 description: None,
393 // A fork is exactly as visible as the repo it came from.
394 is_private: source.is_private,
395 owner_id: a.actor.id.clone(),
396 default_branch: source.default_branch.clone(),
397 fork_of: Some(source.id.clone()),
RFC 3339 timestamps in identity and repos398 created_at: rfc3339(now),
Rust repos service with shipping; pull requests kept in the model399 };
400 self.store
401 .open(&store_key(&source))
402 .await?
403 .fork(&store_key(&fork))
404 .await?;
405 self.registry.insert(&fork).await?;
406 self.publish(NewEvent {
407 kind: "repo.forked",
408 source: SOURCE,
409 repo_id: Some(source.id.clone()),
410 actor: Some(a.actor.id),
411 data: RepoForked {
412 repo_id: fork.id.clone(),
413 source_repo_id: source.id,
Issues and pull requests replace intents and attempts414 pull_id: a.pull_id,
Rust repos service with shipping; pull requests kept in the model415 },
416 })
417 .await?;
418 Ok(Outcome::Ok(fork))
419 }
420
421 async fn git_access(&self, a: GitAccessArgs) -> Result<Outcome<GitAccess>> {
422 let write = a.service == GitService::ReceivePack;
423 // Anonymous callers are asked to authenticate whether or not the repo
424 // exists, so private repos cannot be told apart from missing ones.
425 let denied = || match &a.viewer {
426 Some(_) => not_found(),
427 None => Outcome::fail(FailureCode::Unauthenticated, "Authentication required."),
428 };
429 if let (true, Some(user)) = (write, &a.viewer)
430 && !user.verified
431 {
432 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
433 }
434
435 let repo = match self.registry.by_path(&a.path).await? {
436 Some(repo) => {
437 let allowed = if write {
438 can_write(&repo, &a.viewer)
439 } else {
Members can read a private repository's pull request forks440 self.may_read(&repo, &a.viewer).await?
Rust repos service with shipping; pull requests kept in the model441 };
442 if !allowed {
443 return Ok(denied());
444 }
445 repo
446 }
447 None => {
Workspaces own repositories448 // Push to create, in a workspace the pusher belongs to.
Rust repos service with shipping; pull requests kept in the model449 let owner = a
450 .viewer
451 .as_ref()
Workspaces own repositories452 .filter(|user| write && user.is_member(&a.path.namespace.to_lowercase()));
Rust repos service with shipping; pull requests kept in the model453 let Some(owner) = owner else {
454 return Ok(denied());
455 };
456 let created = self
457 .create(CreateArgs {
458 owner: owner.clone(),
Workspaces own repositories459 namespace: a.path.namespace.clone(),
Rust repos service with shipping; pull requests kept in the model460 name: a.path.name.clone(),
461 description: None,
462 is_private: false,
463 })
464 .await?;
465 match created {
466 Outcome::Ok(repo) => repo,
467 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
468 }
469 }
470 };
471 let git = self.store.open(&store_key(&repo)).await?;
472 let scope = if write { Scope::Write } else { Scope::Read };
473 Ok(Outcome::Ok(git.access(scope).await?))
474 }
475
476 async fn land(&self, a: LandArgs) -> Result<Outcome<Landed>> {
477 let actor: Viewer = Some(a.actor.clone());
Pull requests from branches478 let Some(source) = self.registry.by_id(&a.source_id).await? else {
Rust repos service with shipping; pull requests kept in the model479 return Ok(not_found());
480 };
Pull requests from branches481 // A fork lands on the repository it came from; a branch on its own.
482 let target = match &source.fork_of {
Rust repos service with shipping; pull requests kept in the model483 Some(id) => self.registry.by_id(id).await?,
Pull requests from branches484 None => Some(source.clone()),
Rust repos service with shipping; pull requests kept in the model485 };
486 let Some(target) = target.filter(|repo| can_read(repo, &actor)) else {
487 return Ok(not_found());
488 };
489 if !can_write(&target, &actor) {
490 return Ok(Outcome::fail(
491 FailureCode::Forbidden,
Issues and pull requests replace intents and attempts492 "Only members of the repository's workspace can merge a pull request.",
Rust repos service with shipping; pull requests kept in the model493 ));
494 }
495 if !a.actor.verified {
496 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
497 }
498
499 let branch = &target.default_branch;
Pull requests from branches500 let from_fork = source.id != target.id;
501 let source_branch = match a.branch {
502 Some(name) if !from_fork && name == *branch => {
503 return Ok(Outcome::fail(
504 FailureCode::Invalid,
505 format!("{branch} cannot be merged into itself."),
506 ));
507 }
508 Some(name) => name,
509 None if from_fork => branch.clone(),
510 None => {
511 return Ok(Outcome::fail(
512 FailureCode::Invalid,
513 "Say which branch to merge.",
514 ));
515 }
516 };
517
518 let source_git = self.store.open(&store_key(&source)).await?;
Rust repos service with shipping; pull requests kept in the model519 let target_git = self.store.open(&store_key(&target)).await?;
Pull requests from branches520 let history = source_git.log(&source_branch, MAX_ANCESTRY).await?;
Rust repos service with shipping; pull requests kept in the model521 let Some(new) = history.first().map(|commit| commit.hash.clone()) else {
522 return Ok(Outcome::fail(
523 FailureCode::Conflict,
Issues and pull requests replace intents and attempts524 "This pull request has no commits to merge.",
Rust repos service with shipping; pull requests kept in the model525 ));
526 };
527 let old = target_git
528 .log(branch, 1)
529 .await?
530 .into_iter()
531 .next()
532 .map(|commit| commit.hash);
533
534 if old.as_deref() == Some(new.as_str()) {
Diffs on attempts; hosted agent presented as the g1t agent535 return Ok(Outcome::Ok(Landed {
536 commit: new,
537 previous: None,
538 }));
Rust repos service with shipping; pull requests kept in the model539 }
540 // Moving the branch to a commit that does not descend from its
541 // current head would discard whatever landed in between.
542 if let Some(old) = &old
Pull requests from branches543 && !descends_from(&source_git, &history, old).await?
Rust repos service with shipping; pull requests kept in the model544 {
Pull requests from branches545 let remedy = if from_fork {
546 format!("Pull {branch} into the pull request's fork, push, and merge again.")
547 } else {
548 format!("Merge {branch} into {source_branch}, push, and merge again.")
549 };
Rust repos service with shipping; pull requests kept in the model550 return Ok(Outcome::fail(
551 FailureCode::Conflict,
Pull requests from branches552 format!("{branch} has moved since this pull request was opened. {remedy}"),
Rust repos service with shipping; pull requests kept in the model553 ));
554 }
555
Pull requests from branches556 // For a branch the objects are already in the target; sending them
557 // again is harmless and keeps one way of moving a ref.
558 let source_access = source_git.access(Scope::Read).await?;
Rust repos service with shipping; pull requests kept in the model559 let target_access = target_git.access(Scope::Write).await?;
560 let pushed =
561 land::fast_forward(&source_access, &target_access, branch, old.as_deref(), &new)
562 .await?;
563 if let Err(reason) = pushed {
Issues and pull requests replace intents and attempts564 // Most often another pull request landed between the check and the push.
Rust repos service with shipping; pull requests kept in the model565 return Ok(Outcome::fail(
566 FailureCode::Conflict,
567 format!("{branch} could not be updated: {reason}"),
568 ));
569 }
Events service in Rust, with RFC 3339 times and accurate push events570 self.publish_push(&target, branch, &new, Some(a.actor.id))
571 .await?;
Diffs on attempts; hosted agent presented as the g1t agent572 Ok(Outcome::Ok(Landed {
573 commit: new,
574 previous: old,
575 }))
576 }
577
578 async fn compare(&self, a: CompareArgs) -> Result<Outcome<Comparison>> {
579 let Some(repo) = self
Members can read a private repository's pull request forks580 .visible(self.registry.by_id(&a.repo_id).await?, &a.viewer)
Diffs on attempts; hosted agent presented as the g1t agent581 .await?
582 else {
583 return Ok(not_found());
584 };
585 let git = self.store.open(&store_key(&repo)).await?;
Pull requests from branches586 let head_ref = a.head.as_deref().unwrap_or(&repo.default_branch);
587 let history = git.log(head_ref, MAX_ANCESTRY).await?;
Diffs on attempts; hosted agent presented as the g1t agent588 let Some(head) = history.first() else {
589 return Ok(Outcome::fail(
590 FailureCode::Conflict,
Pull requests from branches591 "There are no commits to compare.",
Diffs on attempts; hosted agent presented as the g1t agent592 ));
593 };
594
Pull requests from branches595 // Where the head's history meets the default branch of `against`.
596 let shared_with = async |against: &Repo| -> Result<Option<String>> {
597 let against_git = self.store.open(&store_key(against)).await?;
598 let shared: HashSet<String> = against_git
599 .log(&against.default_branch, MAX_ANCESTRY)
600 .await?
601 .into_iter()
602 .map(|commit| commit.hash)
603 .collect();
604 nearest_ancestor_in(&git, &history, &shared).await
605 };
Diffs on attempts; hosted agent presented as the g1t agent606 let base = match (a.base, &repo.fork_of) {
607 (Some(base), _) => Some(base),
608 // A fork is compared with the last commit it shares with the
609 // repository it came from.
610 (None, Some(target_id)) => match self.registry.by_id(target_id).await? {
Pull requests from branches611 Some(target) => shared_with(&target).await?,
Diffs on attempts; hosted agent presented as the g1t agent612 None => None,
613 },
Pull requests from branches614 // A branch, with the point where it left the default branch.
615 (None, None) if head_ref != repo.default_branch => shared_with(&repo).await?,
Diffs on attempts; hosted agent presented as the g1t agent616 (None, None) => head.parents.first().cloned(),
617 };
618 let base_tree = match &base {
619 Some(base) => git
620 .log(base, 1)
621 .await?
622 .into_iter()
623 .next()
624 .map(|commit| commit.tree_hash),
625 None => None,
626 };
627 let (files, truncated) =
628 diff::compare_trees(&git, base_tree.as_deref(), &head.tree_hash).await?;
629 Ok(Outcome::Ok(Comparison {
630 base,
631 head: head.hash.clone(),
632 files,
633 truncated,
634 }))
Rust repos service with shipping; pull requests kept in the model635 }
636
Events service in Rust, with RFC 3339 times and accurate push events637 /// Reports that `branch` of `repo` now points to `after`.
638 async fn publish_push(
639 &self,
640 repo: &Repo,
641 branch: &str,
642 after: &str,
643 actor: Option<String>,
644 ) -> Result<()> {
Rust repos service with shipping; pull requests kept in the model645 self.publish(NewEvent {
646 kind: "git.push",
647 source: SOURCE,
648 repo_id: Some(repo.id.clone()),
649 actor,
650 data: GitPush {
651 repo_id: repo.id.clone(),
Events service in Rust, with RFC 3339 times and accurate push events652 git_ref: format!("refs/heads/{branch}"),
Rust repos service with shipping; pull requests kept in the model653 after: after.to_owned(),
Events service in Rust, with RFC 3339 times and accurate push events654 default_branch: branch == repo.default_branch,
Rust repos service with shipping; pull requests kept in the model655 },
656 })
657 .await
658 }
659
660 /// Git over HTTPS.
661 async fn git_http(&self, request: Request, env: &Env) -> Result<Response> {
662 let Some(git) = git_http::parse(&request.url()?) else {
663 return Response::error("Not found", 404);
664 };
665 let viewer = git_http::viewer(&request, &env.service("IDENTITY")?).await?;
666 let access = self
667 .git_access(GitAccessArgs {
668 path: git.path.clone(),
669 viewer: viewer.clone(),
670 service: git.service,
671 })
672 .await?;
673 let access = match access {
674 Outcome::Ok(access) => access,
675 refused => return git_http::refuse(refused),
676 };
Events service in Rust, with RFC 3339 times and accurate push events677 let forwarded = git_http::forward(request, &git, &access).await?;
Rust repos service with shipping; pull requests kept in the model678
679 // Artifacts' own push notifications are per repository, which does
Events service in Rust, with RFC 3339 times and accurate push events680 // not fit a repo per pull request, so the front end reports pushes
681 // itself: one event for each branch that moved.
682 let accepted = forwarded.response.status_code() == 200 && !forwarded.pushed.is_empty();
683 if accepted && let Some(repo) = self.registry.by_path(&git.path).await? {
684 let stored = self.store.open(&store_key(&repo)).await?;
685 let actor = viewer.map(|user: User| user.id);
686 for (branch, pushed) in &forwarded.pushed {
687 // The store can refuse one ref and accept another, so each
688 // is checked against where the branch actually is.
689 let head = stored.log(branch, 1).await?;
690 if head.first().is_some_and(|commit| commit.hash == *pushed) {
691 self.publish_push(&repo, branch, pushed, actor.clone())
692 .await?;
693 }
Rust repos service with shipping; pull requests kept in the model694 }
695 }
Events service in Rust, with RFC 3339 times and accurate push events696 Ok(forwarded.response)
Rust repos service with shipping; pull requests kept in the model697 }
698}
699
700#[event(fetch)]
701async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
702 let repos = Repos {
703 registry: Registry { db: env.d1("DB")? },
704 store: ArtifactsStore::new(&env)?,
Events service in Rust, with RFC 3339 times and accurate push events705 events: env.service("EVENTS")?,
Rust repos service with shipping; pull requests kept in the model706 };
707 let Some(method) = rpc_method(&request) else {
708 return repos.git_http(request, &env).await;
709 };
710 let body: serde_json::Value = request.json().await?;
711
712 match method.as_str() {
713 "get" => reply(&repos.get(args(body)?).await?),
714 "get_by_id" => reply(&repos.get_by_id(args(body)?).await?),
715 "list" => {
716 let a: ListArgs = args(body)?;
717 reply(
718 &repos
719 .registry
Workspaces own repositories720 .list(
721 &a.viewer,
722 a.query.as_deref(),
723 a.namespace.as_deref(),
724 a.member_only,
725 )
Rust repos service with shipping; pull requests kept in the model726 .await?,
727 )
728 }
729 "create" => reply(&repos.create(args(body)?).await?),
730 "tree" => reply(&repos.tree(args(body)?).await?),
731 "blob" => reply(&repos.blob(args(body)?).await?),
732 "log" => reply(&repos.log(args(body)?).await?),
Issues and pull requests replace intents and attempts733 "fork_for_pull" => reply(&repos.fork_for_pull(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model734 "git_access" => reply(&repos.git_access(args(body)?).await?),
Pull requests from branches735 "branches" => reply(&repos.branches(args(body)?).await?),
736 "head" => reply(&repos.head(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model737 "land" => reply(&repos.land(args(body)?).await?),
Diffs on attempts; hosted agent presented as the g1t agent738 "compare" => reply(&repos.compare(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model739 _ => Response::error("Unknown method", 404),
740 }
741}