g1t/services/repos/src/lib.rs

652 lines23,458 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;
11mod registry;
12mod store;
13
14use g1t_contracts::events::{GitPush, NewEvent, RepoCreated, RepoForked};
15use g1t_contracts::repos::*;
RFC 3339 timestamps in identity and repos16use g1t_contracts::time::rfc3339;
Workspaces own repositories17use g1t_contracts::{FailureCode, Outcome, User, Viewer, is_valid_repo_name, new_id};
Rust repos service with shipping; pull requests kept in the model18use g1t_kit::{args, js, now_ms, reply, rpc_method};
Diffs on attempts; hosted agent presented as the g1t agent19use std::collections::{HashMap, HashSet, VecDeque};
Rust repos service with shipping; pull requests kept in the model20
21use serde::Serialize;
22use worker::wasm_bindgen::JsValue;
23use worker::{Context, Env, Request, Response, Result, event};
24
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,
121 /// The events service, an RPC stub.
122 events: JsValue,
123}
124
125impl<S: GitStore> Repos<S> {
126 async fn publish<T: Serialize>(&self, event: NewEvent<T>) -> Result<()> {
127 js::call(&self.events, "publish", &[js::to_js(&[event])?]).await?;
128 Ok(())
129 }
130
131 /// Resolves a repo the viewer may read; private repos look missing.
132 async fn readable(&self, path: &RepoPath, viewer: &Viewer) -> Result<Option<Repo>> {
133 Ok(self
134 .registry
135 .by_path(path)
136 .await?
137 .filter(|repo| can_read(repo, viewer)))
138 }
139
140 async fn get(&self, a: GetArgs) -> Result<Outcome<Repo>> {
141 Ok(self
142 .readable(&a.path, &a.viewer)
143 .await?
144 .map_or_else(not_found, Outcome::Ok))
145 }
146
147 async fn get_by_id(&self, a: GetByIdArgs) -> Result<Outcome<Repo>> {
148 Ok(self
149 .registry
150 .by_id(&a.id)
151 .await?
152 .filter(|repo| can_read(repo, &a.viewer))
153 .map_or_else(not_found, Outcome::Ok))
154 }
155
156 async fn create(&self, a: CreateArgs) -> Result<Outcome<Repo>> {
157 if !a.owner.verified {
158 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
159 }
160 let name = a.name.trim().to_lowercase();
161 if !is_valid_repo_name(&name) {
162 return Ok(Outcome::fail(
163 FailureCode::Invalid,
164 "Use letters, digits, dots, hyphens and underscores only.",
165 ));
166 }
Workspaces own repositories167 let namespace = a.namespace.trim().to_lowercase();
168 if namespace.is_empty() {
Rust repos service with shipping; pull requests kept in the model169 return Ok(Outcome::fail(
170 FailureCode::Invalid,
Workspaces own repositories171 "Say which workspace to create the repository in.",
172 ));
173 }
174 if !a.owner.is_member(&namespace) {
175 return Ok(Outcome::fail(
176 FailureCode::Forbidden,
177 "You are not a member of that workspace.",
Rust repos service with shipping; pull requests kept in the model178 ));
179 }
Workspaces own repositories180 let path = RepoPath { namespace, name };
Rust repos service with shipping; pull requests kept in the model181 if self.registry.by_path(&path).await?.is_some() {
182 return Ok(Outcome::fail(
183 FailureCode::Conflict,
Workspaces own repositories184 "That workspace already has a repository with that name.",
Rust repos service with shipping; pull requests kept in the model185 ));
186 }
187 let now = now_ms();
188 let repo = Repo {
189 id: new_id("rep", now),
190 namespace: path.namespace,
191 name: path.name,
192 description: a
193 .description
194 .map(|text| text.trim().to_owned())
195 .filter(|text| !text.is_empty()),
196 is_private: a.is_private,
197 owner_id: a.owner.id.clone(),
198 default_branch: "main".to_owned(),
199 fork_of: None,
RFC 3339 timestamps in identity and repos200 created_at: rfc3339(now),
Rust repos service with shipping; pull requests kept in the model201 };
202 self.store
203 .create(
204 &store_key(&repo),
205 repo.description.as_deref(),
206 &repo.default_branch,
207 )
208 .await?;
209 self.registry.insert(&repo).await?;
210 self.publish(NewEvent {
211 kind: "repo.created",
212 source: SOURCE,
213 repo_id: Some(repo.id.clone()),
214 actor: Some(a.owner.id),
215 data: RepoCreated {
216 repo_id: repo.id.clone(),
217 namespace: repo.namespace.clone(),
218 name: repo.name.clone(),
219 is_private: repo.is_private,
220 },
221 })
222 .await?;
223 Ok(Outcome::Ok(repo))
224 }
225
226 async fn tree(&self, a: TreeArgs) -> Result<Outcome<TreeView>> {
227 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
228 return Ok(not_found());
229 };
230 let git = self.store.open(&store_key(&repo)).await?;
231 let git_ref = a
232 .git_ref
233 .clone()
234 .unwrap_or_else(|| repo.default_branch.clone());
235
236 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
237 // An unknown ref is an error; a repo with no commits is just empty.
238 if a.git_ref.is_some() {
239 return Ok(Outcome::fail(
240 FailureCode::NotFound,
241 "No such branch, tag or commit.",
242 ));
243 }
244 return Ok(Outcome::Ok(TreeView {
245 repo,
246 git_ref,
247 path: a.tree_path,
248 head: None,
249 entries: Vec::new(),
250 readme: None,
251 }));
252 };
253
254 let no_directory = || Outcome::fail(FailureCode::NotFound, "No such directory.");
255 let mut entries = git.read_tree(&head.tree_hash).await?;
256 for segment in a.tree_path.split('/').filter(|segment| !segment.is_empty()) {
257 let next = entries.as_ref().and_then(|entries| {
258 entries
259 .iter()
260 .find(|entry| entry.name == segment && entry.kind == EntryKind::Tree)
261 });
262 let Some(next) = next else {
263 return Ok(no_directory());
264 };
265 entries = git.read_tree(&next.hash).await?;
266 }
267 let Some(mut entries) = entries else {
268 return Ok(no_directory());
269 };
270 // Directories first, then by name.
271 entries.sort_by(|a, b| {
272 (b.kind == EntryKind::Tree)
273 .cmp(&(a.kind == EntryKind::Tree))
274 .then_with(|| a.name.cmp(&b.name))
275 });
276
277 let readme_entry = entries
278 .iter()
279 .find(|entry| entry.kind == EntryKind::Blob && is_readme(&entry.name));
280 let readme = match readme_entry {
281 Some(entry) => git.read_blob(&entry.hash).await?.map(|bytes| Readme {
282 name: entry.name.clone(),
283 text: text_of(bytes),
284 }),
285 None => None,
286 };
287 Ok(Outcome::Ok(TreeView {
288 repo,
289 git_ref,
290 path: a.tree_path,
291 head: Some(head),
292 entries,
293 readme,
294 }))
295 }
296
297 async fn blob(&self, a: BlobArgs) -> Result<Outcome<BlobView>> {
298 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
299 return Ok(not_found());
300 };
301 let bytes = if a.file_path.is_empty() {
302 None
303 } else {
304 let git = self.store.open(&store_key(&repo)).await?;
305 git.read_file(&a.git_ref, &a.file_path).await?
306 };
307 let Some(bytes) = bytes else {
308 return Ok(Outcome::fail(FailureCode::NotFound, "No such file."));
309 };
310 Ok(Outcome::Ok(BlobView {
311 repo,
312 git_ref: a.git_ref,
313 path: a.file_path,
314 size: bytes.len() as u64,
315 text: text_of(bytes),
316 }))
317 }
318
319 async fn log(&self, a: LogArgs) -> Result<Outcome<Vec<Commit>>> {
320 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
321 return Ok(not_found());
322 };
323 let git = self.store.open(&store_key(&repo)).await?;
324 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
325 Ok(Outcome::Ok(git.log(&git_ref, a.limit).await?))
326 }
327
Issues and pull requests replace intents and attempts328 async fn fork_for_pull(&self, a: ForkArgs) -> Result<Outcome<Repo>> {
Rust repos service with shipping; pull requests kept in the model329 let viewer = Some(a.actor.clone());
330 let Some(source) = self
331 .registry
332 .by_id(&a.source_id)
333 .await?
334 .filter(|repo| can_read(repo, &viewer))
335 else {
336 return Ok(not_found());
337 };
338 let now = now_ms();
339 let fork = Repo {
340 id: new_id("rep", now),
Issues and pull requests replace intents and attempts341 namespace: PULLS_NAMESPACE.to_owned(),
342 name: a.pull_id.clone(),
Rust repos service with shipping; pull requests kept in the model343 description: None,
344 // A fork is exactly as visible as the repo it came from.
345 is_private: source.is_private,
346 owner_id: a.actor.id.clone(),
347 default_branch: source.default_branch.clone(),
348 fork_of: Some(source.id.clone()),
RFC 3339 timestamps in identity and repos349 created_at: rfc3339(now),
Rust repos service with shipping; pull requests kept in the model350 };
351 self.store
352 .open(&store_key(&source))
353 .await?
354 .fork(&store_key(&fork))
355 .await?;
356 self.registry.insert(&fork).await?;
357 self.publish(NewEvent {
358 kind: "repo.forked",
359 source: SOURCE,
360 repo_id: Some(source.id.clone()),
361 actor: Some(a.actor.id),
362 data: RepoForked {
363 repo_id: fork.id.clone(),
364 source_repo_id: source.id,
Issues and pull requests replace intents and attempts365 pull_id: a.pull_id,
Rust repos service with shipping; pull requests kept in the model366 },
367 })
368 .await?;
369 Ok(Outcome::Ok(fork))
370 }
371
372 async fn git_access(&self, a: GitAccessArgs) -> Result<Outcome<GitAccess>> {
373 let write = a.service == GitService::ReceivePack;
374 // Anonymous callers are asked to authenticate whether or not the repo
375 // exists, so private repos cannot be told apart from missing ones.
376 let denied = || match &a.viewer {
377 Some(_) => not_found(),
378 None => Outcome::fail(FailureCode::Unauthenticated, "Authentication required."),
379 };
380 if let (true, Some(user)) = (write, &a.viewer)
381 && !user.verified
382 {
383 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
384 }
385
386 let repo = match self.registry.by_path(&a.path).await? {
387 Some(repo) => {
388 let allowed = if write {
389 can_write(&repo, &a.viewer)
390 } else {
391 can_read(&repo, &a.viewer)
392 };
393 if !allowed {
394 return Ok(denied());
395 }
396 repo
397 }
398 None => {
Workspaces own repositories399 // Push to create, in a workspace the pusher belongs to.
Rust repos service with shipping; pull requests kept in the model400 let owner = a
401 .viewer
402 .as_ref()
Workspaces own repositories403 .filter(|user| write && user.is_member(&a.path.namespace.to_lowercase()));
Rust repos service with shipping; pull requests kept in the model404 let Some(owner) = owner else {
405 return Ok(denied());
406 };
407 let created = self
408 .create(CreateArgs {
409 owner: owner.clone(),
Workspaces own repositories410 namespace: a.path.namespace.clone(),
Rust repos service with shipping; pull requests kept in the model411 name: a.path.name.clone(),
412 description: None,
413 is_private: false,
414 })
415 .await?;
416 match created {
417 Outcome::Ok(repo) => repo,
418 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
419 }
420 }
421 };
422 let git = self.store.open(&store_key(&repo)).await?;
423 let scope = if write { Scope::Write } else { Scope::Read };
424 Ok(Outcome::Ok(git.access(scope).await?))
425 }
426
427 async fn land(&self, a: LandArgs) -> Result<Outcome<Landed>> {
428 let actor: Viewer = Some(a.actor.clone());
429 let Some(fork) = self.registry.by_id(&a.fork_id).await? else {
430 return Ok(not_found());
431 };
432 let target = match &fork.fork_of {
433 Some(id) => self.registry.by_id(id).await?,
434 None => None,
435 };
436 let Some(target) = target.filter(|repo| can_read(repo, &actor)) else {
437 return Ok(not_found());
438 };
439 if !can_write(&target, &actor) {
440 return Ok(Outcome::fail(
441 FailureCode::Forbidden,
Issues and pull requests replace intents and attempts442 "Only members of the repository's workspace can merge a pull request.",
Rust repos service with shipping; pull requests kept in the model443 ));
444 }
445 if !a.actor.verified {
446 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
447 }
448
449 let branch = &target.default_branch;
450 let fork_git = self.store.open(&store_key(&fork)).await?;
451 let target_git = self.store.open(&store_key(&target)).await?;
452 let history = fork_git.log(branch, MAX_ANCESTRY).await?;
453 let Some(new) = history.first().map(|commit| commit.hash.clone()) else {
454 return Ok(Outcome::fail(
455 FailureCode::Conflict,
Issues and pull requests replace intents and attempts456 "This pull request has no commits to merge.",
Rust repos service with shipping; pull requests kept in the model457 ));
458 };
459 let old = target_git
460 .log(branch, 1)
461 .await?
462 .into_iter()
463 .next()
464 .map(|commit| commit.hash);
465
466 if old.as_deref() == Some(new.as_str()) {
Diffs on attempts; hosted agent presented as the g1t agent467 return Ok(Outcome::Ok(Landed {
468 commit: new,
469 previous: None,
470 }));
Rust repos service with shipping; pull requests kept in the model471 }
472 // Moving the branch to a commit that does not descend from its
473 // current head would discard whatever landed in between.
474 if let Some(old) = &old
475 && !descends_from(&fork_git, &history, old).await?
476 {
477 return Ok(Outcome::fail(
478 FailureCode::Conflict,
479 format!(
Issues and pull requests replace intents and attempts480 "{branch} has moved since this pull request was opened. Pull {branch} into the pull request's fork, push, and merge again."
Rust repos service with shipping; pull requests kept in the model481 ),
482 ));
483 }
484
485 let source_access = fork_git.access(Scope::Read).await?;
486 let target_access = target_git.access(Scope::Write).await?;
487 let pushed =
488 land::fast_forward(&source_access, &target_access, branch, old.as_deref(), &new)
489 .await?;
490 if let Err(reason) = pushed {
Issues and pull requests replace intents and attempts491 // Most often another pull request landed between the check and the push.
Rust repos service with shipping; pull requests kept in the model492 return Ok(Outcome::fail(
493 FailureCode::Conflict,
494 format!("{branch} could not be updated: {reason}"),
495 ));
496 }
497 self.publish_push(&target, &new, Some(a.actor.id)).await?;
Diffs on attempts; hosted agent presented as the g1t agent498 Ok(Outcome::Ok(Landed {
499 commit: new,
500 previous: old,
501 }))
502 }
503
504 async fn compare(&self, a: CompareArgs) -> Result<Outcome<Comparison>> {
505 let Some(repo) = self
506 .registry
507 .by_id(&a.repo_id)
508 .await?
509 .filter(|repo| can_read(repo, &a.viewer))
510 else {
511 return Ok(not_found());
512 };
513 let git = self.store.open(&store_key(&repo)).await?;
514 let history = git.log(&repo.default_branch, MAX_ANCESTRY).await?;
515 let Some(head) = history.first() else {
516 return Ok(Outcome::fail(
517 FailureCode::Conflict,
518 "This repository has no commits yet.",
519 ));
520 };
521
522 let base = match (a.base, &repo.fork_of) {
523 (Some(base), _) => Some(base),
524 // A fork is compared with the last commit it shares with the
525 // repository it came from.
526 (None, Some(target_id)) => match self.registry.by_id(target_id).await? {
527 Some(target) => {
528 let target_git = self.store.open(&store_key(&target)).await?;
529 let shared: HashSet<String> = target_git
530 .log(&target.default_branch, MAX_ANCESTRY)
531 .await?
532 .into_iter()
533 .map(|commit| commit.hash)
534 .collect();
535 nearest_ancestor_in(&git, &history, &shared).await?
536 }
537 None => None,
538 },
539 (None, None) => head.parents.first().cloned(),
540 };
541 let base_tree = match &base {
542 Some(base) => git
543 .log(base, 1)
544 .await?
545 .into_iter()
546 .next()
547 .map(|commit| commit.tree_hash),
548 None => None,
549 };
550 let (files, truncated) =
551 diff::compare_trees(&git, base_tree.as_deref(), &head.tree_hash).await?;
552 Ok(Outcome::Ok(Comparison {
553 base,
554 head: head.hash.clone(),
555 files,
556 truncated,
557 }))
Rust repos service with shipping; pull requests kept in the model558 }
559
560 async fn publish_push(&self, repo: &Repo, after: &str, actor: Option<String>) -> Result<()> {
561 self.publish(NewEvent {
562 kind: "git.push",
563 source: SOURCE,
564 repo_id: Some(repo.id.clone()),
565 actor,
566 data: GitPush {
567 repo_id: repo.id.clone(),
568 git_ref: format!("refs/heads/{}", repo.default_branch),
569 after: after.to_owned(),
570 },
571 })
572 .await
573 }
574
575 /// Git over HTTPS.
576 async fn git_http(&self, request: Request, env: &Env) -> Result<Response> {
577 let Some(git) = git_http::parse(&request.url()?) else {
578 return Response::error("Not found", 404);
579 };
580 let viewer = git_http::viewer(&request, &env.service("IDENTITY")?).await?;
581 let access = self
582 .git_access(GitAccessArgs {
583 path: git.path.clone(),
584 viewer: viewer.clone(),
585 service: git.service,
586 })
587 .await?;
588 let access = match access {
589 Outcome::Ok(access) => access,
590 refused => return git_http::refuse(refused),
591 };
592 let response = git_http::forward(request, &git, &access).await?;
593
594 // Artifacts' own push notifications are per repository, which does
Issues and pull requests replace intents and attempts595 // not fit a repo per pull request, so the front end reports pushes itself.
Rust repos service with shipping; pull requests kept in the model596 let pushed = git.endpoint == "git-receive-pack" && response.status_code() == 200;
597 if pushed && let Some(repo) = self.registry.by_path(&git.path).await? {
598 let head = self
599 .store
600 .open(&store_key(&repo))
601 .await?
602 .log(&repo.default_branch, 1)
603 .await?;
604 if let Some(head) = head.first() {
605 self.publish_push(&repo, &head.hash, viewer.map(|user: User| user.id))
606 .await?;
607 }
608 }
609 Ok(response)
610 }
611}
612
613#[event(fetch)]
614async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
615 let repos = Repos {
616 registry: Registry { db: env.d1("DB")? },
617 store: ArtifactsStore::new(&env)?,
618 events: js::binding(&env, "EVENTS")?,
619 };
620 let Some(method) = rpc_method(&request) else {
621 return repos.git_http(request, &env).await;
622 };
623 let body: serde_json::Value = request.json().await?;
624
625 match method.as_str() {
626 "get" => reply(&repos.get(args(body)?).await?),
627 "get_by_id" => reply(&repos.get_by_id(args(body)?).await?),
628 "list" => {
629 let a: ListArgs = args(body)?;
630 reply(
631 &repos
632 .registry
Workspaces own repositories633 .list(
634 &a.viewer,
635 a.query.as_deref(),
636 a.namespace.as_deref(),
637 a.member_only,
638 )
Rust repos service with shipping; pull requests kept in the model639 .await?,
640 )
641 }
642 "create" => reply(&repos.create(args(body)?).await?),
643 "tree" => reply(&repos.tree(args(body)?).await?),
644 "blob" => reply(&repos.blob(args(body)?).await?),
645 "log" => reply(&repos.log(args(body)?).await?),
Issues and pull requests replace intents and attempts646 "fork_for_pull" => reply(&repos.fork_for_pull(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model647 "git_access" => reply(&repos.git_access(args(body)?).await?),
648 "land" => reply(&repos.land(args(body)?).await?),
Diffs on attempts; hosted agent presented as the g1t agent649 "compare" => reply(&repos.compare(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model650 _ => Response::error("Unknown method", 404),
651 }
652}