g1t/services/repos/src/lib.rs

707 lines25,824 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
15use g1t_contracts::events::{GitPush, NewEvent, RepoCreated, RepoForked};
16use 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};
Rust repos service with shipping; pull requests kept in the model19use g1t_kit::{args, js, 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;
23use worker::wasm_bindgen::JsValue;
24use worker::{Context, Env, Request, Response, Result, event};
25
26use registry::{Registry, can_read, can_write, store_key};
27use store::{ArtifactsStore, GitRepo, GitStore, Scope};
28
Issues and pull requests replace intents and attempts29/// Namespace that holds every pull request's fork: `pulls/<pull id>`.
30const PULLS_NAMESPACE: &str = "pulls";
Rust repos service with shipping; pull requests kept in the model31const MAX_TEXT_BYTES: usize = 512 * 1024;
Issues and pull requests replace intents and attempts32/// How far back a pull request may have forked and still be landed.
Rust repos service with shipping; pull requests kept in the model33const MAX_ANCESTRY: u32 = 1000;
34const SOURCE: &str = "repos";
35const UNVERIFIED: &str = "Confirm your email address first. Check your inbox, or resend the link from the banner on g1t.sh.";
36
37fn not_found<T>() -> Outcome<T> {
38 Outcome::fail(FailureCode::NotFound, "Repository not found.")
39}
40
41/// Decoded text, or `None` when the file is too large or looks binary.
42fn text_of(bytes: Vec<u8>) -> Option<String> {
43 if bytes.len() > MAX_TEXT_BYTES || bytes.contains(&0) {
44 return None;
45 }
46 Some(String::from_utf8_lossy(&bytes).into_owned())
47}
48
49fn is_readme(name: &str) -> bool {
50 matches!(
51 name.to_lowercase().as_str(),
52 "readme" | "readme.md" | "readme.markdown" | "readme.txt"
53 )
54}
55
56/// Whether `ancestor` is reachable from the newest commit in `history`.
57///
58/// `history` is the first-parent chain, which is all the store lists; a fork
59/// that merged the target branch in has the target's head on a second
60/// parent, so the walk follows every parent.
61async fn descends_from<R: GitRepo>(repo: &R, history: &[Commit], ancestor: &str) -> Result<bool> {
62 let known: HashMap<&str, &[String]> = history
63 .iter()
64 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
65 .collect();
66 let mut seen = HashSet::new();
67 let mut queue: Vec<String> = history
68 .first()
69 .map(|c| c.hash.clone())
70 .into_iter()
71 .collect();
72 while let Some(hash) = queue.pop() {
73 if hash == ancestor {
74 return Ok(true);
75 }
76 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
77 continue;
78 }
79 match known.get(hash.as_str()) {
80 Some(parents) => queue.extend(parents.iter().cloned()),
81 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
82 }
83 }
84 Ok(false)
85}
86
Diffs on attempts; hosted agent presented as the g1t agent87/// The commit closest to the newest in `history` that is also in `shared`:
88/// where a fork and the repository it came from last agreed.
89async fn nearest_ancestor_in<R: GitRepo>(
90 repo: &R,
91 history: &[Commit],
92 shared: &HashSet<String>,
93) -> Result<Option<String>> {
94 let known: HashMap<&str, &[String]> = history
95 .iter()
96 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
97 .collect();
98 let mut seen = HashSet::new();
99 let mut queue: VecDeque<String> = history
100 .first()
101 .map(|c| c.hash.clone())
102 .into_iter()
103 .collect();
104 while let Some(hash) = queue.pop_front() {
105 if shared.contains(&hash) {
106 return Ok(Some(hash));
107 }
108 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
109 continue;
110 }
111 match known.get(hash.as_str()) {
112 Some(parents) => queue.extend(parents.iter().cloned()),
113 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
114 }
115 }
116 Ok(None)
117}
118
Rust repos service with shipping; pull requests kept in the model119struct Repos<S: GitStore> {
120 registry: Registry,
121 store: S,
122 /// The events service, an RPC stub.
123 events: JsValue,
124}
125
126impl<S: GitStore> Repos<S> {
127 async fn publish<T: Serialize>(&self, event: NewEvent<T>) -> Result<()> {
128 js::call(&self.events, "publish", &[js::to_js(&[event])?]).await?;
129 Ok(())
130 }
131
132 /// Resolves a repo the viewer may read; private repos look missing.
133 async fn readable(&self, path: &RepoPath, viewer: &Viewer) -> Result<Option<Repo>> {
134 Ok(self
135 .registry
136 .by_path(path)
137 .await?
138 .filter(|repo| can_read(repo, viewer)))
139 }
140
141 async fn get(&self, a: GetArgs) -> Result<Outcome<Repo>> {
142 Ok(self
143 .readable(&a.path, &a.viewer)
144 .await?
145 .map_or_else(not_found, Outcome::Ok))
146 }
147
148 async fn get_by_id(&self, a: GetByIdArgs) -> Result<Outcome<Repo>> {
149 Ok(self
150 .registry
151 .by_id(&a.id)
152 .await?
153 .filter(|repo| can_read(repo, &a.viewer))
154 .map_or_else(not_found, Outcome::Ok))
155 }
156
157 async fn create(&self, a: CreateArgs) -> Result<Outcome<Repo>> {
158 if !a.owner.verified {
159 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
160 }
161 let name = a.name.trim().to_lowercase();
162 if !is_valid_repo_name(&name) {
163 return Ok(Outcome::fail(
164 FailureCode::Invalid,
165 "Use letters, digits, dots, hyphens and underscores only.",
166 ));
167 }
Workspaces own repositories168 let namespace = a.namespace.trim().to_lowercase();
169 if namespace.is_empty() {
Rust repos service with shipping; pull requests kept in the model170 return Ok(Outcome::fail(
171 FailureCode::Invalid,
Workspaces own repositories172 "Say which workspace to create the repository in.",
173 ));
174 }
175 if !a.owner.is_member(&namespace) {
176 return Ok(Outcome::fail(
177 FailureCode::Forbidden,
178 "You are not a member of that workspace.",
Rust repos service with shipping; pull requests kept in the model179 ));
180 }
Workspaces own repositories181 let path = RepoPath { namespace, name };
Rust repos service with shipping; pull requests kept in the model182 if self.registry.by_path(&path).await?.is_some() {
183 return Ok(Outcome::fail(
184 FailureCode::Conflict,
Workspaces own repositories185 "That workspace already has a repository with that name.",
Rust repos service with shipping; pull requests kept in the model186 ));
187 }
188 let now = now_ms();
189 let repo = Repo {
190 id: new_id("rep", now),
191 namespace: path.namespace,
192 name: path.name,
193 description: a
194 .description
195 .map(|text| text.trim().to_owned())
196 .filter(|text| !text.is_empty()),
197 is_private: a.is_private,
198 owner_id: a.owner.id.clone(),
199 default_branch: "main".to_owned(),
200 fork_of: None,
RFC 3339 timestamps in identity and repos201 created_at: rfc3339(now),
Rust repos service with shipping; pull requests kept in the model202 };
203 self.store
204 .create(
205 &store_key(&repo),
206 repo.description.as_deref(),
207 &repo.default_branch,
208 )
209 .await?;
210 self.registry.insert(&repo).await?;
211 self.publish(NewEvent {
212 kind: "repo.created",
213 source: SOURCE,
214 repo_id: Some(repo.id.clone()),
215 actor: Some(a.owner.id),
216 data: RepoCreated {
217 repo_id: repo.id.clone(),
218 namespace: repo.namespace.clone(),
219 name: repo.name.clone(),
220 is_private: repo.is_private,
221 },
222 })
223 .await?;
224 Ok(Outcome::Ok(repo))
225 }
226
227 async fn tree(&self, a: TreeArgs) -> Result<Outcome<TreeView>> {
228 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
229 return Ok(not_found());
230 };
231 let git = self.store.open(&store_key(&repo)).await?;
232 let git_ref = a
233 .git_ref
234 .clone()
235 .unwrap_or_else(|| repo.default_branch.clone());
236
237 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
238 // An unknown ref is an error; a repo with no commits is just empty.
239 if a.git_ref.is_some() {
240 return Ok(Outcome::fail(
241 FailureCode::NotFound,
242 "No such branch, tag or commit.",
243 ));
244 }
245 return Ok(Outcome::Ok(TreeView {
246 repo,
247 git_ref,
248 path: a.tree_path,
249 head: None,
250 entries: Vec::new(),
251 readme: None,
252 }));
253 };
254
255 let no_directory = || Outcome::fail(FailureCode::NotFound, "No such directory.");
256 let mut entries = git.read_tree(&head.tree_hash).await?;
257 for segment in a.tree_path.split('/').filter(|segment| !segment.is_empty()) {
258 let next = entries.as_ref().and_then(|entries| {
259 entries
260 .iter()
261 .find(|entry| entry.name == segment && entry.kind == EntryKind::Tree)
262 });
263 let Some(next) = next else {
264 return Ok(no_directory());
265 };
266 entries = git.read_tree(&next.hash).await?;
267 }
268 let Some(mut entries) = entries else {
269 return Ok(no_directory());
270 };
271 // Directories first, then by name.
272 entries.sort_by(|a, b| {
273 (b.kind == EntryKind::Tree)
274 .cmp(&(a.kind == EntryKind::Tree))
275 .then_with(|| a.name.cmp(&b.name))
276 });
277
278 let readme_entry = entries
279 .iter()
280 .find(|entry| entry.kind == EntryKind::Blob && is_readme(&entry.name));
281 let readme = match readme_entry {
282 Some(entry) => git.read_blob(&entry.hash).await?.map(|bytes| Readme {
283 name: entry.name.clone(),
284 text: text_of(bytes),
285 }),
286 None => None,
287 };
288 Ok(Outcome::Ok(TreeView {
289 repo,
290 git_ref,
291 path: a.tree_path,
292 head: Some(head),
293 entries,
294 readme,
295 }))
296 }
297
298 async fn blob(&self, a: BlobArgs) -> Result<Outcome<BlobView>> {
299 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
300 return Ok(not_found());
301 };
302 let bytes = if a.file_path.is_empty() {
303 None
304 } else {
305 let git = self.store.open(&store_key(&repo)).await?;
306 git.read_file(&a.git_ref, &a.file_path).await?
307 };
308 let Some(bytes) = bytes else {
309 return Ok(Outcome::fail(FailureCode::NotFound, "No such file."));
310 };
311 Ok(Outcome::Ok(BlobView {
312 repo,
313 git_ref: a.git_ref,
314 path: a.file_path,
315 size: bytes.len() as u64,
316 text: text_of(bytes),
317 }))
318 }
319
320 async fn log(&self, a: LogArgs) -> Result<Outcome<Vec<Commit>>> {
321 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
322 return Ok(not_found());
323 };
324 let git = self.store.open(&store_key(&repo)).await?;
325 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
326 Ok(Outcome::Ok(git.log(&git_ref, a.limit).await?))
327 }
328
Pull requests from branches329 /// The repository's branches, default branch first.
330 async fn branches(&self, a: BranchesArgs) -> Result<Outcome<Vec<Branch>>> {
331 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
332 return Ok(not_found());
333 };
334 let mut branches = self.store.open(&store_key(&repo)).await?.branches().await?;
335 branches.sort_by_key(|branch| branch.name != repo.default_branch);
336 Ok(Outcome::Ok(branches))
337 }
338
339 async fn head(&self, a: HeadArgs) -> Result<Option<String>> {
340 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
341 return Ok(None);
342 };
343 let git = self.store.open(&store_key(&repo)).await?;
344 Ok(git
345 .log(&a.branch, 1)
346 .await?
347 .into_iter()
348 .next()
349 .map(|commit| commit.hash))
350 }
351
Issues and pull requests replace intents and attempts352 async fn fork_for_pull(&self, a: ForkArgs) -> Result<Outcome<Repo>> {
Rust repos service with shipping; pull requests kept in the model353 let viewer = Some(a.actor.clone());
354 let Some(source) = self
355 .registry
356 .by_id(&a.source_id)
357 .await?
358 .filter(|repo| can_read(repo, &viewer))
359 else {
360 return Ok(not_found());
361 };
362 let now = now_ms();
363 let fork = Repo {
364 id: new_id("rep", now),
Issues and pull requests replace intents and attempts365 namespace: PULLS_NAMESPACE.to_owned(),
366 name: a.pull_id.clone(),
Rust repos service with shipping; pull requests kept in the model367 description: None,
368 // A fork is exactly as visible as the repo it came from.
369 is_private: source.is_private,
370 owner_id: a.actor.id.clone(),
371 default_branch: source.default_branch.clone(),
372 fork_of: Some(source.id.clone()),
RFC 3339 timestamps in identity and repos373 created_at: rfc3339(now),
Rust repos service with shipping; pull requests kept in the model374 };
375 self.store
376 .open(&store_key(&source))
377 .await?
378 .fork(&store_key(&fork))
379 .await?;
380 self.registry.insert(&fork).await?;
381 self.publish(NewEvent {
382 kind: "repo.forked",
383 source: SOURCE,
384 repo_id: Some(source.id.clone()),
385 actor: Some(a.actor.id),
386 data: RepoForked {
387 repo_id: fork.id.clone(),
388 source_repo_id: source.id,
Issues and pull requests replace intents and attempts389 pull_id: a.pull_id,
Rust repos service with shipping; pull requests kept in the model390 },
391 })
392 .await?;
393 Ok(Outcome::Ok(fork))
394 }
395
396 async fn git_access(&self, a: GitAccessArgs) -> Result<Outcome<GitAccess>> {
397 let write = a.service == GitService::ReceivePack;
398 // Anonymous callers are asked to authenticate whether or not the repo
399 // exists, so private repos cannot be told apart from missing ones.
400 let denied = || match &a.viewer {
401 Some(_) => not_found(),
402 None => Outcome::fail(FailureCode::Unauthenticated, "Authentication required."),
403 };
404 if let (true, Some(user)) = (write, &a.viewer)
405 && !user.verified
406 {
407 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
408 }
409
410 let repo = match self.registry.by_path(&a.path).await? {
411 Some(repo) => {
412 let allowed = if write {
413 can_write(&repo, &a.viewer)
414 } else {
415 can_read(&repo, &a.viewer)
416 };
417 if !allowed {
418 return Ok(denied());
419 }
420 repo
421 }
422 None => {
Workspaces own repositories423 // Push to create, in a workspace the pusher belongs to.
Rust repos service with shipping; pull requests kept in the model424 let owner = a
425 .viewer
426 .as_ref()
Workspaces own repositories427 .filter(|user| write && user.is_member(&a.path.namespace.to_lowercase()));
Rust repos service with shipping; pull requests kept in the model428 let Some(owner) = owner else {
429 return Ok(denied());
430 };
431 let created = self
432 .create(CreateArgs {
433 owner: owner.clone(),
Workspaces own repositories434 namespace: a.path.namespace.clone(),
Rust repos service with shipping; pull requests kept in the model435 name: a.path.name.clone(),
436 description: None,
437 is_private: false,
438 })
439 .await?;
440 match created {
441 Outcome::Ok(repo) => repo,
442 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
443 }
444 }
445 };
446 let git = self.store.open(&store_key(&repo)).await?;
447 let scope = if write { Scope::Write } else { Scope::Read };
448 Ok(Outcome::Ok(git.access(scope).await?))
449 }
450
451 async fn land(&self, a: LandArgs) -> Result<Outcome<Landed>> {
452 let actor: Viewer = Some(a.actor.clone());
Pull requests from branches453 let Some(source) = self.registry.by_id(&a.source_id).await? else {
Rust repos service with shipping; pull requests kept in the model454 return Ok(not_found());
455 };
Pull requests from branches456 // A fork lands on the repository it came from; a branch on its own.
457 let target = match &source.fork_of {
Rust repos service with shipping; pull requests kept in the model458 Some(id) => self.registry.by_id(id).await?,
Pull requests from branches459 None => Some(source.clone()),
Rust repos service with shipping; pull requests kept in the model460 };
461 let Some(target) = target.filter(|repo| can_read(repo, &actor)) else {
462 return Ok(not_found());
463 };
464 if !can_write(&target, &actor) {
465 return Ok(Outcome::fail(
466 FailureCode::Forbidden,
Issues and pull requests replace intents and attempts467 "Only members of the repository's workspace can merge a pull request.",
Rust repos service with shipping; pull requests kept in the model468 ));
469 }
470 if !a.actor.verified {
471 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
472 }
473
474 let branch = &target.default_branch;
Pull requests from branches475 let from_fork = source.id != target.id;
476 let source_branch = match a.branch {
477 Some(name) if !from_fork && name == *branch => {
478 return Ok(Outcome::fail(
479 FailureCode::Invalid,
480 format!("{branch} cannot be merged into itself."),
481 ));
482 }
483 Some(name) => name,
484 None if from_fork => branch.clone(),
485 None => {
486 return Ok(Outcome::fail(
487 FailureCode::Invalid,
488 "Say which branch to merge.",
489 ));
490 }
491 };
492
493 let source_git = self.store.open(&store_key(&source)).await?;
Rust repos service with shipping; pull requests kept in the model494 let target_git = self.store.open(&store_key(&target)).await?;
Pull requests from branches495 let history = source_git.log(&source_branch, MAX_ANCESTRY).await?;
Rust repos service with shipping; pull requests kept in the model496 let Some(new) = history.first().map(|commit| commit.hash.clone()) else {
497 return Ok(Outcome::fail(
498 FailureCode::Conflict,
Issues and pull requests replace intents and attempts499 "This pull request has no commits to merge.",
Rust repos service with shipping; pull requests kept in the model500 ));
501 };
502 let old = target_git
503 .log(branch, 1)
504 .await?
505 .into_iter()
506 .next()
507 .map(|commit| commit.hash);
508
509 if old.as_deref() == Some(new.as_str()) {
Diffs on attempts; hosted agent presented as the g1t agent510 return Ok(Outcome::Ok(Landed {
511 commit: new,
512 previous: None,
513 }));
Rust repos service with shipping; pull requests kept in the model514 }
515 // Moving the branch to a commit that does not descend from its
516 // current head would discard whatever landed in between.
517 if let Some(old) = &old
Pull requests from branches518 && !descends_from(&source_git, &history, old).await?
Rust repos service with shipping; pull requests kept in the model519 {
Pull requests from branches520 let remedy = if from_fork {
521 format!("Pull {branch} into the pull request's fork, push, and merge again.")
522 } else {
523 format!("Merge {branch} into {source_branch}, push, and merge again.")
524 };
Rust repos service with shipping; pull requests kept in the model525 return Ok(Outcome::fail(
526 FailureCode::Conflict,
Pull requests from branches527 format!("{branch} has moved since this pull request was opened. {remedy}"),
Rust repos service with shipping; pull requests kept in the model528 ));
529 }
530
Pull requests from branches531 // For a branch the objects are already in the target; sending them
532 // again is harmless and keeps one way of moving a ref.
533 let source_access = source_git.access(Scope::Read).await?;
Rust repos service with shipping; pull requests kept in the model534 let target_access = target_git.access(Scope::Write).await?;
535 let pushed =
536 land::fast_forward(&source_access, &target_access, branch, old.as_deref(), &new)
537 .await?;
538 if let Err(reason) = pushed {
Issues and pull requests replace intents and attempts539 // Most often another pull request landed between the check and the push.
Rust repos service with shipping; pull requests kept in the model540 return Ok(Outcome::fail(
541 FailureCode::Conflict,
542 format!("{branch} could not be updated: {reason}"),
543 ));
544 }
545 self.publish_push(&target, &new, Some(a.actor.id)).await?;
Diffs on attempts; hosted agent presented as the g1t agent546 Ok(Outcome::Ok(Landed {
547 commit: new,
548 previous: old,
549 }))
550 }
551
552 async fn compare(&self, a: CompareArgs) -> Result<Outcome<Comparison>> {
553 let Some(repo) = self
554 .registry
555 .by_id(&a.repo_id)
556 .await?
557 .filter(|repo| can_read(repo, &a.viewer))
558 else {
559 return Ok(not_found());
560 };
561 let git = self.store.open(&store_key(&repo)).await?;
Pull requests from branches562 let head_ref = a.head.as_deref().unwrap_or(&repo.default_branch);
563 let history = git.log(head_ref, MAX_ANCESTRY).await?;
Diffs on attempts; hosted agent presented as the g1t agent564 let Some(head) = history.first() else {
565 return Ok(Outcome::fail(
566 FailureCode::Conflict,
Pull requests from branches567 "There are no commits to compare.",
Diffs on attempts; hosted agent presented as the g1t agent568 ));
569 };
570
Pull requests from branches571 // Where the head's history meets the default branch of `against`.
572 let shared_with = async |against: &Repo| -> Result<Option<String>> {
573 let against_git = self.store.open(&store_key(against)).await?;
574 let shared: HashSet<String> = against_git
575 .log(&against.default_branch, MAX_ANCESTRY)
576 .await?
577 .into_iter()
578 .map(|commit| commit.hash)
579 .collect();
580 nearest_ancestor_in(&git, &history, &shared).await
581 };
Diffs on attempts; hosted agent presented as the g1t agent582 let base = match (a.base, &repo.fork_of) {
583 (Some(base), _) => Some(base),
584 // A fork is compared with the last commit it shares with the
585 // repository it came from.
586 (None, Some(target_id)) => match self.registry.by_id(target_id).await? {
Pull requests from branches587 Some(target) => shared_with(&target).await?,
Diffs on attempts; hosted agent presented as the g1t agent588 None => None,
589 },
Pull requests from branches590 // A branch, with the point where it left the default branch.
591 (None, None) if head_ref != repo.default_branch => shared_with(&repo).await?,
Diffs on attempts; hosted agent presented as the g1t agent592 (None, None) => head.parents.first().cloned(),
593 };
594 let base_tree = match &base {
595 Some(base) => git
596 .log(base, 1)
597 .await?
598 .into_iter()
599 .next()
600 .map(|commit| commit.tree_hash),
601 None => None,
602 };
603 let (files, truncated) =
604 diff::compare_trees(&git, base_tree.as_deref(), &head.tree_hash).await?;
605 Ok(Outcome::Ok(Comparison {
606 base,
607 head: head.hash.clone(),
608 files,
609 truncated,
610 }))
Rust repos service with shipping; pull requests kept in the model611 }
612
613 async fn publish_push(&self, repo: &Repo, after: &str, actor: Option<String>) -> Result<()> {
614 self.publish(NewEvent {
615 kind: "git.push",
616 source: SOURCE,
617 repo_id: Some(repo.id.clone()),
618 actor,
619 data: GitPush {
620 repo_id: repo.id.clone(),
621 git_ref: format!("refs/heads/{}", repo.default_branch),
622 after: after.to_owned(),
623 },
624 })
625 .await
626 }
627
628 /// Git over HTTPS.
629 async fn git_http(&self, request: Request, env: &Env) -> Result<Response> {
630 let Some(git) = git_http::parse(&request.url()?) else {
631 return Response::error("Not found", 404);
632 };
633 let viewer = git_http::viewer(&request, &env.service("IDENTITY")?).await?;
634 let access = self
635 .git_access(GitAccessArgs {
636 path: git.path.clone(),
637 viewer: viewer.clone(),
638 service: git.service,
639 })
640 .await?;
641 let access = match access {
642 Outcome::Ok(access) => access,
643 refused => return git_http::refuse(refused),
644 };
645 let response = git_http::forward(request, &git, &access).await?;
646
647 // Artifacts' own push notifications are per repository, which does
Issues and pull requests replace intents and attempts648 // not fit a repo per pull request, so the front end reports pushes itself.
Rust repos service with shipping; pull requests kept in the model649 let pushed = git.endpoint == "git-receive-pack" && response.status_code() == 200;
650 if pushed && let Some(repo) = self.registry.by_path(&git.path).await? {
651 let head = self
652 .store
653 .open(&store_key(&repo))
654 .await?
655 .log(&repo.default_branch, 1)
656 .await?;
657 if let Some(head) = head.first() {
658 self.publish_push(&repo, &head.hash, viewer.map(|user: User| user.id))
659 .await?;
660 }
661 }
662 Ok(response)
663 }
664}
665
666#[event(fetch)]
667async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
668 let repos = Repos {
669 registry: Registry { db: env.d1("DB")? },
670 store: ArtifactsStore::new(&env)?,
671 events: js::binding(&env, "EVENTS")?,
672 };
673 let Some(method) = rpc_method(&request) else {
674 return repos.git_http(request, &env).await;
675 };
676 let body: serde_json::Value = request.json().await?;
677
678 match method.as_str() {
679 "get" => reply(&repos.get(args(body)?).await?),
680 "get_by_id" => reply(&repos.get_by_id(args(body)?).await?),
681 "list" => {
682 let a: ListArgs = args(body)?;
683 reply(
684 &repos
685 .registry
Workspaces own repositories686 .list(
687 &a.viewer,
688 a.query.as_deref(),
689 a.namespace.as_deref(),
690 a.member_only,
691 )
Rust repos service with shipping; pull requests kept in the model692 .await?,
693 )
694 }
695 "create" => reply(&repos.create(args(body)?).await?),
696 "tree" => reply(&repos.tree(args(body)?).await?),
697 "blob" => reply(&repos.blob(args(body)?).await?),
698 "log" => reply(&repos.log(args(body)?).await?),
Issues and pull requests replace intents and attempts699 "fork_for_pull" => reply(&repos.fork_for_pull(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model700 "git_access" => reply(&repos.git_access(args(body)?).await?),
Pull requests from branches701 "branches" => reply(&repos.branches(args(body)?).await?),
702 "head" => reply(&repos.head(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model703 "land" => reply(&repos.land(args(body)?).await?),
Diffs on attempts; hosted agent presented as the g1t agent704 "compare" => reply(&repos.compare(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model705 _ => Response::error("Unknown method", 404),
706 }
707}