flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/services/repos/src/lib.rs

1,225 lines47,289 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;
Catching up with main takes seconds when the two sides touched different files9mod catch_up;
Diffs on attempts; hosted agent presented as the g1t agent10mod diff;
Rust repos service with shipping; pull requests kept in the model11mod git_http;
Agents as a team: lifecycle, merge queue, billing and a new shell12mod import;
Rust repos service with shipping; pull requests kept in the model13mod land;
Search across all of g1t, Explore, and a command palette14mod listing;
Pull requests from branches15mod refs;
Rust repos service with shipping; pull requests kept in the model16mod registry;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API17mod run_access;
18mod secret_scan;
Rust repos service with shipping; pull requests kept in the model19mod store;
20
Agents and memory, checks and conflicts, profiles, slug renames, custom domains21use g1t_contracts::events::{
Search across all of g1t, Explore, and a command palette22 Event, GitPush, NewEvent, Publish, RepoCreated, RepoForked, RepoUpdated, RepoVisibilityChanged,
23 WorkspaceRenamed,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains24};
Rust repos service with shipping; pull requests kept in the model25use g1t_contracts::repos::*;
RFC 3339 timestamps in identity and repos26use g1t_contracts::time::rfc3339;
Agents as a team: lifecycle, merge queue, billing and a new shell27use g1t_contracts::{FailureCode, Outcome, PrincipalKind, User, Viewer, is_valid_repo_name, new_id};
Events service in Rust, with RFC 3339 times and accurate push events28use g1t_kit::{args, now_ms, reply, rpc_method};
Diffs on attempts; hosted agent presented as the g1t agent29use std::collections::{HashMap, HashSet, VecDeque};
Rust repos service with shipping; pull requests kept in the model30
31use serde::Serialize;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains32use worker::{Context, Env, Fetcher, MessageBatch, Method, Request, Response, Result, event};
Rust repos service with shipping; pull requests kept in the model33
34use registry::{Registry, can_read, can_write, store_key};
35use store::{ArtifactsStore, GitRepo, GitStore, Scope};
36
Issues and pull requests replace intents and attempts37/// Namespace that holds every pull request's fork: `pulls/<pull id>`.
38const PULLS_NAMESPACE: &str = "pulls";
Rust repos service with shipping; pull requests kept in the model39const MAX_TEXT_BYTES: usize = 512 * 1024;
Issues and pull requests replace intents and attempts40/// How far back a pull request may have forked and still be landed.
Rust repos service with shipping; pull requests kept in the model41const MAX_ANCESTRY: u32 = 1000;
Agents as a team: lifecycle, merge queue, billing and a new shell42const MAX_DESCRIPTION_CHARS: usize = 200;
Rust repos service with shipping; pull requests kept in the model43const SOURCE: &str = "repos";
44const UNVERIFIED: &str = "Confirm your email address first. Check your inbox, or resend the link from the banner on g1t.sh.";
45
46fn not_found<T>() -> Outcome<T> {
47 Outcome::fail(FailureCode::NotFound, "Repository not found.")
48}
49
50/// Decoded text, or `None` when the file is too large or looks binary.
Agents as a team: lifecycle, merge queue, billing and a new shell51/// Whether a ref is a full commit hash rather than a branch name.
52fn is_commit_hash(git_ref: &str) -> bool {
53 git_ref.len() == 40 && git_ref.bytes().all(|b| b.is_ascii_hexdigit())
54}
55
Rust repos service with shipping; pull requests kept in the model56fn text_of(bytes: Vec<u8>) -> Option<String> {
57 if bytes.len() > MAX_TEXT_BYTES || bytes.contains(&0) {
58 return None;
59 }
60 Some(String::from_utf8_lossy(&bytes).into_owned())
61}
62
63fn is_readme(name: &str) -> bool {
64 matches!(
65 name.to_lowercase().as_str(),
66 "readme" | "readme.md" | "readme.markdown" | "readme.txt"
67 )
68}
69
70/// Whether `ancestor` is reachable from the newest commit in `history`.
71///
72/// `history` is the first-parent chain, which is all the store lists; a fork
73/// that merged the target branch in has the target's head on a second
74/// parent, so the walk follows every parent.
75async fn descends_from<R: GitRepo>(repo: &R, history: &[Commit], ancestor: &str) -> Result<bool> {
76 let known: HashMap<&str, &[String]> = history
77 .iter()
78 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
79 .collect();
80 let mut seen = HashSet::new();
81 let mut queue: Vec<String> = history
82 .first()
83 .map(|c| c.hash.clone())
84 .into_iter()
85 .collect();
86 while let Some(hash) = queue.pop() {
87 if hash == ancestor {
88 return Ok(true);
89 }
90 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
91 continue;
92 }
93 match known.get(hash.as_str()) {
94 Some(parents) => queue.extend(parents.iter().cloned()),
95 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
96 }
97 }
98 Ok(false)
99}
100
Diffs on attempts; hosted agent presented as the g1t agent101/// The commit closest to the newest in `history` that is also in `shared`:
102/// where a fork and the repository it came from last agreed.
103async fn nearest_ancestor_in<R: GitRepo>(
104 repo: &R,
105 history: &[Commit],
106 shared: &HashSet<String>,
107) -> Result<Option<String>> {
108 let known: HashMap<&str, &[String]> = history
109 .iter()
110 .map(|commit| (commit.hash.as_str(), commit.parents.as_slice()))
111 .collect();
112 let mut seen = HashSet::new();
113 let mut queue: VecDeque<String> = history
114 .first()
115 .map(|c| c.hash.clone())
116 .into_iter()
117 .collect();
118 while let Some(hash) = queue.pop_front() {
119 if shared.contains(&hash) {
120 return Ok(Some(hash));
121 }
122 if !seen.insert(hash.clone()) || seen.len() > MAX_ANCESTRY as usize {
123 continue;
124 }
125 match known.get(hash.as_str()) {
126 Some(parents) => queue.extend(parents.iter().cloned()),
127 None => queue.extend(repo.parents(&hash).await?.unwrap_or_default()),
128 }
129 }
130 Ok(None)
131}
132
Rust repos service with shipping; pull requests kept in the model133struct Repos<S: GitStore> {
134 registry: Registry,
135 store: S,
Events service in Rust, with RFC 3339 times and accurate push events136 events: Fetcher,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API137 /// Asked during a push which secrets have been allowed.
138 security: Option<Fetcher>,
Rust repos service with shipping; pull requests kept in the model139}
140
141impl<S: GitStore> Repos<S> {
142 async fn publish<T: Serialize>(&self, event: NewEvent<T>) -> Result<()> {
Events service in Rust, with RFC 3339 times and accurate push events143 g1t_kit::call(
144 &self.events,
145 "publish",
146 &Publish {
147 events: vec![event],
148 },
149 )
150 .await
Rust repos service with shipping; pull requests kept in the model151 }
152
Members can read a private repository's pull request forks153 /// Whether the viewer may read `repo`. A pull request's fork of a
154 /// private repository can be read by everyone who can read that
155 /// repository, so its members can review and check out the change, as
156 /// well as by whoever opened the pull request.
157 async fn may_read(&self, repo: &Repo, viewer: &Viewer) -> Result<bool> {
158 if can_read(repo, viewer) {
159 return Ok(true);
160 }
161 let Some(source_id) = &repo.fork_of else {
162 return Ok(false);
163 };
Rust repos service with shipping; pull requests kept in the model164 Ok(self
165 .registry
Members can read a private repository's pull request forks166 .by_id(source_id)
Rust repos service with shipping; pull requests kept in the model167 .await?
Members can read a private repository's pull request forks168 .is_some_and(|source| can_read(&source, viewer)))
Rust repos service with shipping; pull requests kept in the model169 }
170
Members can read a private repository's pull request forks171 /// `repo`, if there is one and the viewer may read it.
172 async fn visible(&self, repo: Option<Repo>, viewer: &Viewer) -> Result<Option<Repo>> {
173 Ok(match repo {
174 Some(repo) if self.may_read(&repo, viewer).await? => Some(repo),
175 _ => None,
176 })
177 }
178
179 /// Resolves a repo the viewer may read; private repos look missing.
180 async fn readable(&self, path: &RepoPath, viewer: &Viewer) -> Result<Option<Repo>> {
181 self.visible(self.registry.by_path(path).await?, viewer)
182 .await
183 }
184
Rust repos service with shipping; pull requests kept in the model185 async fn get(&self, a: GetArgs) -> Result<Outcome<Repo>> {
186 Ok(self
187 .readable(&a.path, &a.viewer)
188 .await?
189 .map_or_else(not_found, Outcome::Ok))
190 }
191
192 async fn get_by_id(&self, a: GetByIdArgs) -> Result<Outcome<Repo>> {
193 Ok(self
Members can read a private repository's pull request forks194 .visible(self.registry.by_id(&a.id).await?, &a.viewer)
Rust repos service with shipping; pull requests kept in the model195 .await?
196 .map_or_else(not_found, Outcome::Ok))
197 }
198
Agents as a team: lifecycle, merge queue, billing and a new shell199 async fn update(&self, a: UpdateArgs) -> Result<Outcome<Repo>> {
200 let viewer = Some(a.actor.clone());
201 let Some(repo) = self.readable(&a.path, &viewer).await? else {
202 return Ok(not_found());
203 };
204 if repo.fork_of.is_some() || !can_write(&repo, &viewer) {
205 return Ok(Outcome::fail(
206 FailureCode::Forbidden,
207 "Only members of the repository's workspace can change its settings.",
208 ));
209 }
210 if !a.actor.verified {
211 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
212 }
213 let description = match a.description {
214 Some(text) => Some(
215 text.trim()
216 .chars()
217 .take(MAX_DESCRIPTION_CHARS)
218 .collect::<String>(),
219 )
220 .filter(|text| !text.is_empty()),
221 None => repo.description.clone(),
222 };
223 let is_private = a.is_private.unwrap_or(repo.is_private);
224 let protected = a.protected.unwrap_or(repo.protected);
Search across all of g1t, Explore, and a command palette225 let topics = match &a.topics {
226 Some(topics) => match clean_topics(topics) {
227 Ok(topics) => topics,
228 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
229 },
230 None => repo.topics.clone(),
231 };
Agents as a team: lifecycle, merge queue, billing and a new shell232 self.registry
Search across all of g1t, Explore, and a command palette233 .update(&repo.id, description.as_deref(), is_private, protected, &topics)
Agents as a team: lifecycle, merge queue, billing and a new shell234 .await?;
Search across all of g1t, Explore, and a command palette235 let visibility_changed = is_private != repo.is_private;
236 let updated = Repo {
Agents as a team: lifecycle, merge queue, billing and a new shell237 description,
238 is_private,
239 protected,
Search across all of g1t, Explore, and a command palette240 topics,
Agents as a team: lifecycle, merge queue, billing and a new shell241 ..repo
Search across all of g1t, Explore, and a command palette242 };
243 // Search and anything else that shows the repository hears of it;
244 // a change of visibility is announced on its own as well, so that
245 // what was public stops being shown at once.
246 self.publish(NewEvent {
247 kind: "repo.updated",
248 source: SOURCE,
249 repo_id: Some(updated.id.clone()),
250 actor: Some(a.actor.id.clone()),
251 data: RepoUpdated {
252 repo_id: updated.id.clone(),
253 namespace: updated.namespace.clone(),
254 name: updated.name.clone(),
255 is_private,
256 visibility_changed,
257 },
258 })
259 .await?;
260 if visibility_changed {
261 self.publish(NewEvent {
262 kind: "repo.visibility_changed",
263 source: SOURCE,
264 repo_id: Some(updated.id.clone()),
265 actor: Some(a.actor.id),
266 data: RepoVisibilityChanged {
267 repo_id: updated.id.clone(),
268 is_private,
269 },
270 })
271 .await?;
272 }
273 Ok(Outcome::Ok(updated))
274 }
275
276 /// The repository with this id, if it is not a fork, and its store.
277 async fn stored(&self, repo_id: &str) -> Result<Option<S::Repo>> {
278 match self.registry.by_id(repo_id).await? {
279 Some(repo) if repo.fork_of.is_none() => Ok(Some(self.store.open(&store_key(&repo)).await?)),
280 _ => Ok(None),
281 }
282 }
283
284 async fn list_files(&self, a: ListFilesArgs) -> Result<FileList> {
285 let Some(repo) = self.registry.by_id(&a.repo_id).await?.filter(|repo| repo.fork_of.is_none()) else {
286 return Ok(FileList::default());
287 };
288 let git = self.store.open(&store_key(&repo)).await?;
289 let head = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
290 listing::list(&git, None, &head, &a.skip_dirs, a.limit).await
291 }
292
293 async fn changed_files(&self, a: ChangedFilesArgs) -> Result<FileList> {
294 let Some(git) = self.stored(&a.repo_id).await? else {
295 return Ok(FileList::default());
296 };
297 listing::list(&git, a.base.as_deref(), &a.head, &a.skip_dirs, a.limit).await
298 }
299
300 async fn read_blobs(&self, a: ReadBlobsArgs) -> Result<Vec<BlobText>> {
301 let Some(git) = self.stored(&a.repo_id).await? else {
302 return Ok(Vec::new());
303 };
304 listing::read(&git, &a.hashes, a.max_bytes.min(MAX_TEXT_BYTES as u32)).await
Agents as a team: lifecycle, merge queue, billing and a new shell305 }
306
Rust repos service with shipping; pull requests kept in the model307 async fn create(&self, a: CreateArgs) -> Result<Outcome<Repo>> {
308 if !a.owner.verified {
309 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
310 }
311 let name = a.name.trim().to_lowercase();
312 if !is_valid_repo_name(&name) {
313 return Ok(Outcome::fail(
314 FailureCode::Invalid,
315 "Use letters, digits, dots, hyphens and underscores only.",
316 ));
317 }
Workspaces own repositories318 let namespace = a.namespace.trim().to_lowercase();
319 if namespace.is_empty() {
Rust repos service with shipping; pull requests kept in the model320 return Ok(Outcome::fail(
321 FailureCode::Invalid,
Workspaces own repositories322 "Say which workspace to create the repository in.",
323 ));
324 }
325 if !a.owner.is_member(&namespace) {
326 return Ok(Outcome::fail(
327 FailureCode::Forbidden,
328 "You are not a member of that workspace.",
Rust repos service with shipping; pull requests kept in the model329 ));
330 }
Workspaces own repositories331 let path = RepoPath { namespace, name };
Rust repos service with shipping; pull requests kept in the model332 if self.registry.by_path(&path).await?.is_some() {
333 return Ok(Outcome::fail(
334 FailureCode::Conflict,
Workspaces own repositories335 "That workspace already has a repository with that name.",
Rust repos service with shipping; pull requests kept in the model336 ));
337 }
Agents as a team: lifecycle, merge queue, billing and a new shell338 // An import is fetched before anything is created, so that an
339 // address that does not work leaves nothing behind.
340 let mut imported = None;
341 if let Some(url) = a
342 .import_url
343 .as_deref()
344 .map(str::trim)
345 .filter(|url| !url.is_empty())
346 {
347 let Some(url) = import::clean_url(url) else {
348 return Ok(Outcome::fail(
349 FailureCode::Invalid,
350 "Give the https address of a public repository, such as https://github.com/owner/repo.",
351 ));
352 };
353 let remote = match import::discover(&url).await? {
354 Ok(remote) => remote,
355 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
356 };
357 let pack = match import::fetch(&url, &remote.head).await? {
358 Ok(pack) => pack,
359 Err(reason) => return Ok(Outcome::fail(FailureCode::Invalid, reason)),
360 };
361 imported = Some((remote, pack));
362 }
Rust repos service with shipping; pull requests kept in the model363 let now = now_ms();
364 let repo = Repo {
365 id: new_id("rep", now),
366 namespace: path.namespace,
367 name: path.name,
368 description: a
369 .description
370 .map(|text| text.trim().to_owned())
371 .filter(|text| !text.is_empty()),
372 is_private: a.is_private,
373 owner_id: a.owner.id.clone(),
Agents as a team: lifecycle, merge queue, billing and a new shell374 default_branch: imported
375 .as_ref()
376 .map_or_else(|| "main".to_owned(), |(remote, _)| remote.branch.clone()),
Rust repos service with shipping; pull requests kept in the model377 fork_of: None,
Agents as a team: lifecycle, merge queue, billing and a new shell378 protected: false,
RFC 3339 timestamps in identity and repos379 created_at: rfc3339(now),
Search across all of g1t, Explore, and a command palette380 topics: Vec::new(),
Rust repos service with shipping; pull requests kept in the model381 };
Agents and memory, checks and conflicts, profiles, slug renames, custom domains382 self.registry.claim_store_key(&repo).await?;
Rust repos service with shipping; pull requests kept in the model383 self.store
384 .create(
385 &store_key(&repo),
386 repo.description.as_deref(),
387 &repo.default_branch,
388 )
389 .await?;
390 self.registry.insert(&repo).await?;
Agents as a team: lifecycle, merge queue, billing and a new shell391 let mut pushed = None;
392 if let Some((remote, pack)) = imported {
393 let access = self
394 .store
395 .open(&store_key(&repo))
396 .await?
397 .access(Scope::Write)
398 .await?;
399 let stored =
400 land::push_pack(&access, &repo.default_branch, None, &remote.head, pack).await?;
401 if let Err(reason) = stored {
402 self.registry.remove(&repo.id).await?;
403 return Ok(Outcome::fail(
404 FailureCode::Invalid,
405 format!("The repository could not be stored: {reason}"),
406 ));
407 }
408 pushed = Some(remote.head);
409 }
Rust repos service with shipping; pull requests kept in the model410 self.publish(NewEvent {
411 kind: "repo.created",
412 source: SOURCE,
413 repo_id: Some(repo.id.clone()),
414 actor: Some(a.owner.id),
415 data: RepoCreated {
416 repo_id: repo.id.clone(),
417 namespace: repo.namespace.clone(),
418 name: repo.name.clone(),
419 is_private: repo.is_private,
420 },
421 })
422 .await?;
Agents as a team: lifecycle, merge queue, billing and a new shell423 if let Some(head) = pushed {
GitHub Actions on g1t, part one: reading workflows424 self.publish_push(
425 &repo,
426 &format!("refs/heads/{}", repo.default_branch),
427 None,
428 &head,
429 None,
430 )
Agents as a team: lifecycle, merge queue, billing and a new shell431 .await?;
432 }
Rust repos service with shipping; pull requests kept in the model433 Ok(Outcome::Ok(repo))
434 }
435
436 async fn tree(&self, a: TreeArgs) -> Result<Outcome<TreeView>> {
437 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
438 return Ok(not_found());
439 };
440 let git = self.store.open(&store_key(&repo)).await?;
441 let git_ref = a
442 .git_ref
443 .clone()
444 .unwrap_or_else(|| repo.default_branch.clone());
445
446 let Some(head) = git.log(&git_ref, 1).await?.into_iter().next() else {
447 // An unknown ref is an error; a repo with no commits is just empty.
448 if a.git_ref.is_some() {
449 return Ok(Outcome::fail(
450 FailureCode::NotFound,
451 "No such branch, tag or commit.",
452 ));
453 }
454 return Ok(Outcome::Ok(TreeView {
455 repo,
456 git_ref,
457 path: a.tree_path,
458 head: None,
459 entries: Vec::new(),
460 readme: None,
461 }));
462 };
463
464 let no_directory = || Outcome::fail(FailureCode::NotFound, "No such directory.");
465 let mut entries = git.read_tree(&head.tree_hash).await?;
466 for segment in a.tree_path.split('/').filter(|segment| !segment.is_empty()) {
467 let next = entries.as_ref().and_then(|entries| {
468 entries
469 .iter()
470 .find(|entry| entry.name == segment && entry.kind == EntryKind::Tree)
471 });
472 let Some(next) = next else {
473 return Ok(no_directory());
474 };
475 entries = git.read_tree(&next.hash).await?;
476 }
477 let Some(mut entries) = entries else {
478 return Ok(no_directory());
479 };
480 // Directories first, then by name.
481 entries.sort_by(|a, b| {
482 (b.kind == EntryKind::Tree)
483 .cmp(&(a.kind == EntryKind::Tree))
484 .then_with(|| a.name.cmp(&b.name))
485 });
486
487 let readme_entry = entries
488 .iter()
489 .find(|entry| entry.kind == EntryKind::Blob && is_readme(&entry.name));
490 let readme = match readme_entry {
491 Some(entry) => git.read_blob(&entry.hash).await?.map(|bytes| Readme {
492 name: entry.name.clone(),
493 text: text_of(bytes),
494 }),
495 None => None,
496 };
497 Ok(Outcome::Ok(TreeView {
498 repo,
499 git_ref,
500 path: a.tree_path,
501 head: Some(head),
502 entries,
503 readme,
504 }))
505 }
506
507 async fn blob(&self, a: BlobArgs) -> Result<Outcome<BlobView>> {
508 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
509 return Ok(not_found());
510 };
511 let bytes = if a.file_path.is_empty() {
512 None
513 } else {
514 let git = self.store.open(&store_key(&repo)).await?;
515 git.read_file(&a.git_ref, &a.file_path).await?
516 };
517 let Some(bytes) = bytes else {
518 return Ok(Outcome::fail(FailureCode::NotFound, "No such file."));
519 };
520 Ok(Outcome::Ok(BlobView {
521 repo,
522 git_ref: a.git_ref,
523 path: a.file_path,
524 size: bytes.len() as u64,
525 text: text_of(bytes),
526 }))
527 }
528
Agents as a team: lifecycle, merge queue, billing and a new shell529 async fn blame(&self, a: BlameArgs) -> Result<Outcome<Blame>> {
530 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
531 return Ok(not_found());
532 };
533 let git = self.store.open(&store_key(&repo)).await?;
534 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
535 Ok(match blame::blame(&git, &git_ref, &a.file_path).await? {
536 Some(blame) => Outcome::Ok(blame),
537 None => not_found(),
538 })
539 }
540
Rust repos service with shipping; pull requests kept in the model541 async fn log(&self, a: LogArgs) -> Result<Outcome<Vec<Commit>>> {
542 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
543 return Ok(not_found());
544 };
545 let git = self.store.open(&store_key(&repo)).await?;
546 let git_ref = a.git_ref.unwrap_or_else(|| repo.default_branch.clone());
547 Ok(Outcome::Ok(git.log(&git_ref, a.limit).await?))
548 }
549
Pull requests from branches550 /// The repository's branches, default branch first.
551 async fn branches(&self, a: BranchesArgs) -> Result<Outcome<Vec<Branch>>> {
552 let Some(repo) = self.readable(&a.path, &a.viewer).await? else {
553 return Ok(not_found());
554 };
555 let mut branches = self.store.open(&store_key(&repo)).await?.branches().await?;
556 branches.sort_by_key(|branch| branch.name != repo.default_branch);
557 Ok(Outcome::Ok(branches))
558 }
559
Agents as a team: lifecycle, merge queue, billing and a new shell560 /// Whether a pull request's source lacks commits that the branch it
561 /// would merge into has.
562 async fn behind(&self, a: BehindArgs) -> Result<bool> {
563 let Some(source) = self.registry.by_id(&a.source_id).await? else {
564 return Ok(false);
565 };
566 let target = match &source.fork_of {
567 Some(id) => self.registry.by_id(id).await?,
568 None => Some(source.clone()),
569 };
570 let Some(target) = target else {
571 return Ok(false);
572 };
573 let branch = a.branch.unwrap_or_else(|| target.default_branch.clone());
574 let target_head = self
575 .store
576 .open(&store_key(&target))
577 .await?
578 .log(&target.default_branch, 1)
579 .await?
580 .into_iter()
581 .next()
582 .map(|commit| commit.hash);
583 let Some(target_head) = target_head else {
584 return Ok(false);
585 };
586 let source_git = self.store.open(&store_key(&source)).await?;
587 let history = source_git.log(&branch, MAX_ANCESTRY).await?;
588 if history.is_empty() {
589 return Ok(false);
590 }
591 Ok(!descends_from(&source_git, &history, &target_head).await?)
592 }
593
Agents and memory, checks and conflicts, profiles, slug renames, custom domains594 /// The files a pull request's source and the default branch it would
595 /// merge into each changed since they last agreed. Where the two lists
596 /// share no file, the merge cannot conflict; where they do, it may.
597 async fn divergence(&self, a: BehindArgs) -> Result<Option<Divergence>> {
598 let Some(source) = self.registry.by_id(&a.source_id).await? else {
599 return Ok(None);
600 };
601 let target = match &source.fork_of {
602 Some(id) => self.registry.by_id(id).await?,
603 None => Some(source.clone()),
604 };
605 let Some(target) = target else {
606 return Ok(None);
607 };
608 let branch = a.branch.unwrap_or_else(|| target.default_branch.clone());
609 let source_git = self.store.open(&store_key(&source)).await?;
610 let target_git = self.store.open(&store_key(&target)).await?;
611 let (history, target_history) = futures_util::future::try_join(
612 source_git.log(&branch, MAX_ANCESTRY),
613 target_git.log(&target.default_branch, MAX_ANCESTRY),
614 )
615 .await?;
616 let (Some(head), Some(base)) = (history.first(), target_history.first()) else {
617 return Ok(None);
618 };
619 let behind = !descends_from(&source_git, &history, &base.hash).await?;
620 let shared: HashSet<String> = target_history.iter().map(|commit| commit.hash.clone()).collect();
621 let merge_base = nearest_ancestor_in(&source_git, &history, &shared).await?;
622 let mut divergence = Divergence {
623 head: head.hash.clone(),
624 base: base.hash.clone(),
625 merge_base: merge_base.clone(),
626 behind,
627 ..Divergence::default()
628 };
629 let merge_base_tree = match &merge_base {
630 Some(hash) => target_history
631 .iter()
632 .find(|commit| commit.hash == *hash)
633 .map(|commit| commit.tree_hash.clone()),
634 None => None,
635 };
636 let Some(merge_base_tree) = merge_base_tree else {
637 // No common history to compare from: say nothing is known.
638 divergence.truncated = true;
639 return Ok(Some(divergence));
640 };
641 let (ours, truncated_ours) =
642 diff::changed_paths(&source_git, Some(&merge_base_tree), &head.tree_hash).await?;
643 divergence.ours = ours;
644 divergence.truncated = truncated_ours;
645 if behind {
646 let (theirs, truncated_theirs) =
647 diff::changed_paths(&target_git, Some(&merge_base_tree), &base.tree_hash).await?;
648 divergence.theirs = theirs;
649 divergence.truncated |= truncated_theirs;
650 }
651 Ok(Some(divergence))
652 }
653
Pull requests from branches654 async fn head(&self, a: HeadArgs) -> Result<Option<String>> {
655 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
656 return Ok(None);
657 };
Workflows run when an agent's pull request is marked ready658 let branch = if a.branch.is_empty() { &repo.default_branch } else { &a.branch };
Pull requests from branches659 let git = self.store.open(&store_key(&repo)).await?;
660 Ok(git
Workflows run when an agent's pull request is marked ready661 .log(branch, 1)
Pull requests from branches662 .await?
663 .into_iter()
664 .next()
665 .map(|commit| commit.hash))
666 }
667
Merge queue: tested states are deleted once their entry leaves668 async fn delete_branch(&self, a: DeleteBranchArgs) -> Result<Outcome<bool>> {
669 if !a.branch.starts_with(G1T_BRANCH_PREFIX) {
670 return Ok(Outcome::fail(
671 FailureCode::Forbidden,
672 "Only branches g1t made for itself can be deleted this way.",
673 ));
674 }
675 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
676 return Ok(not_found());
677 };
678 let git = self.store.open(&store_key(&repo)).await?;
679 let Some(old) = git
680 .branches()
681 .await?
682 .into_iter()
683 .find(|branch| branch.name == a.branch)
684 .map(|branch| branch.hash)
685 else {
686 return Ok(Outcome::Ok(false));
687 };
688 let access = git.access(Scope::Write).await?;
689 if let Err(reason) = land::delete_ref(&access, &a.branch, &old).await? {
690 return Ok(Outcome::fail(
691 FailureCode::Conflict,
692 format!("{} could not be deleted: {reason}", a.branch),
693 ));
694 }
695 Ok(Outcome::Ok(true))
696 }
697
Issues and pull requests replace intents and attempts698 async fn fork_for_pull(&self, a: ForkArgs) -> Result<Outcome<Repo>> {
Rust repos service with shipping; pull requests kept in the model699 let viewer = Some(a.actor.clone());
700 let Some(source) = self
701 .registry
702 .by_id(&a.source_id)
703 .await?
704 .filter(|repo| can_read(repo, &viewer))
705 else {
706 return Ok(not_found());
707 };
708 let now = now_ms();
709 let fork = Repo {
710 id: new_id("rep", now),
Issues and pull requests replace intents and attempts711 namespace: PULLS_NAMESPACE.to_owned(),
712 name: a.pull_id.clone(),
Rust repos service with shipping; pull requests kept in the model713 description: None,
714 // A fork is exactly as visible as the repo it came from.
715 is_private: source.is_private,
716 owner_id: a.actor.id.clone(),
717 default_branch: source.default_branch.clone(),
718 fork_of: Some(source.id.clone()),
Agents as a team: lifecycle, merge queue, billing and a new shell719 protected: false,
RFC 3339 timestamps in identity and repos720 created_at: rfc3339(now),
Search across all of g1t, Explore, and a command palette721 topics: Vec::new(),
Rust repos service with shipping; pull requests kept in the model722 };
Agents and memory, checks and conflicts, profiles, slug renames, custom domains723 self.registry.claim_store_key(&fork).await?;
Rust repos service with shipping; pull requests kept in the model724 self.store
725 .open(&store_key(&source))
726 .await?
727 .fork(&store_key(&fork))
728 .await?;
729 self.registry.insert(&fork).await?;
730 self.publish(NewEvent {
731 kind: "repo.forked",
732 source: SOURCE,
733 repo_id: Some(source.id.clone()),
734 actor: Some(a.actor.id),
735 data: RepoForked {
736 repo_id: fork.id.clone(),
737 source_repo_id: source.id,
Issues and pull requests replace intents and attempts738 pull_id: a.pull_id,
Rust repos service with shipping; pull requests kept in the model739 },
740 })
741 .await?;
742 Ok(Outcome::Ok(fork))
743 }
744
745 async fn git_access(&self, a: GitAccessArgs) -> Result<Outcome<GitAccess>> {
746 let write = a.service == GitService::ReceivePack;
747 // Anonymous callers are asked to authenticate whether or not the repo
748 // exists, so private repos cannot be told apart from missing ones.
749 let denied = || match &a.viewer {
750 Some(_) => not_found(),
751 None => Outcome::fail(FailureCode::Unauthenticated, "Authentication required."),
752 };
Agents as a team: lifecycle, merge queue, billing and a new shell753 // An agent's token works through the API only: its sandbox has its
754 // own way to push, to its own pull request.
755 if a.viewer.as_ref().is_some_and(|user| user.kind == PrincipalKind::Agent) {
756 return Ok(Outcome::fail(
757 FailureCode::Forbidden,
758 "A g1t agent's token cannot be used with git.",
759 ));
760 }
Rust repos service with shipping; pull requests kept in the model761 if let (true, Some(user)) = (write, &a.viewer)
762 && !user.verified
763 {
764 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
765 }
766
767 let repo = match self.registry.by_path(&a.path).await? {
768 Some(repo) => {
769 let allowed = if write {
770 can_write(&repo, &a.viewer)
771 } else {
Members can read a private repository's pull request forks772 self.may_read(&repo, &a.viewer).await?
Rust repos service with shipping; pull requests kept in the model773 };
774 if !allowed {
775 return Ok(denied());
776 }
777 repo
778 }
779 None => {
Workspaces own repositories780 // Push to create, in a workspace the pusher belongs to.
Rust repos service with shipping; pull requests kept in the model781 let owner = a
782 .viewer
783 .as_ref()
Workspaces own repositories784 .filter(|user| write && user.is_member(&a.path.namespace.to_lowercase()));
Rust repos service with shipping; pull requests kept in the model785 let Some(owner) = owner else {
786 return Ok(denied());
787 };
788 let created = self
789 .create(CreateArgs {
790 owner: owner.clone(),
Workspaces own repositories791 namespace: a.path.namespace.clone(),
Rust repos service with shipping; pull requests kept in the model792 name: a.path.name.clone(),
793 description: None,
794 is_private: false,
Agents as a team: lifecycle, merge queue, billing and a new shell795 import_url: None,
Rust repos service with shipping; pull requests kept in the model796 })
797 .await?;
798 match created {
799 Outcome::Ok(repo) => repo,
800 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
801 }
802 }
803 };
804 let git = self.store.open(&store_key(&repo)).await?;
805 let scope = if write { Scope::Write } else { Scope::Read };
806 Ok(Outcome::Ok(git.access(scope).await?))
807 }
808
809 async fn land(&self, a: LandArgs) -> Result<Outcome<Landed>> {
810 let actor: Viewer = Some(a.actor.clone());
Pull requests from branches811 let Some(source) = self.registry.by_id(&a.source_id).await? else {
Rust repos service with shipping; pull requests kept in the model812 return Ok(not_found());
813 };
Pull requests from branches814 // A fork lands on the repository it came from; a branch on its own.
815 let target = match &source.fork_of {
Rust repos service with shipping; pull requests kept in the model816 Some(id) => self.registry.by_id(id).await?,
Pull requests from branches817 None => Some(source.clone()),
Rust repos service with shipping; pull requests kept in the model818 };
819 let Some(target) = target.filter(|repo| can_read(repo, &actor)) else {
820 return Ok(not_found());
821 };
822 if !can_write(&target, &actor) {
823 return Ok(Outcome::fail(
824 FailureCode::Forbidden,
Issues and pull requests replace intents and attempts825 "Only members of the repository's workspace can merge a pull request.",
Rust repos service with shipping; pull requests kept in the model826 ));
827 }
828 if !a.actor.verified {
829 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
830 }
831
832 let branch = &target.default_branch;
Pull requests from branches833 let from_fork = source.id != target.id;
834 let source_branch = match a.branch {
835 Some(name) if !from_fork && name == *branch => {
836 return Ok(Outcome::fail(
837 FailureCode::Invalid,
838 format!("{branch} cannot be merged into itself."),
839 ));
840 }
841 Some(name) => name,
842 None if from_fork => branch.clone(),
843 None => {
844 return Ok(Outcome::fail(
845 FailureCode::Invalid,
846 "Say which branch to merge.",
847 ));
848 }
849 };
850
851 let source_git = self.store.open(&store_key(&source)).await?;
Rust repos service with shipping; pull requests kept in the model852 let target_git = self.store.open(&store_key(&target)).await?;
Pull requests from branches853 let history = source_git.log(&source_branch, MAX_ANCESTRY).await?;
Rust repos service with shipping; pull requests kept in the model854 let Some(new) = history.first().map(|commit| commit.hash.clone()) else {
855 return Ok(Outcome::fail(
856 FailureCode::Conflict,
Issues and pull requests replace intents and attempts857 "This pull request has no commits to merge.",
Rust repos service with shipping; pull requests kept in the model858 ));
859 };
860 let old = target_git
861 .log(branch, 1)
862 .await?
863 .into_iter()
864 .next()
865 .map(|commit| commit.hash);
866
867 if old.as_deref() == Some(new.as_str()) {
Diffs on attempts; hosted agent presented as the g1t agent868 return Ok(Outcome::Ok(Landed {
869 commit: new,
870 previous: None,
871 }));
Rust repos service with shipping; pull requests kept in the model872 }
873 // Moving the branch to a commit that does not descend from its
874 // current head would discard whatever landed in between.
875 if let Some(old) = &old
Pull requests from branches876 && !descends_from(&source_git, &history, old).await?
Rust repos service with shipping; pull requests kept in the model877 {
Pull requests from branches878 let remedy = if from_fork {
879 format!("Pull {branch} into the pull request's fork, push, and merge again.")
880 } else {
881 format!("Merge {branch} into {source_branch}, push, and merge again.")
882 };
Rust repos service with shipping; pull requests kept in the model883 return Ok(Outcome::fail(
884 FailureCode::Conflict,
Pull requests from branches885 format!("{branch} has moved since this pull request was opened. {remedy}"),
Rust repos service with shipping; pull requests kept in the model886 ));
887 }
888
Pull requests from branches889 // For a branch the objects are already in the target; sending them
890 // again is harmless and keeps one way of moving a ref.
891 let source_access = source_git.access(Scope::Read).await?;
Rust repos service with shipping; pull requests kept in the model892 let target_access = target_git.access(Scope::Write).await?;
893 let pushed =
894 land::fast_forward(&source_access, &target_access, branch, old.as_deref(), &new)
895 .await?;
896 if let Err(reason) = pushed {
Issues and pull requests replace intents and attempts897 // Most often another pull request landed between the check and the push.
Rust repos service with shipping; pull requests kept in the model898 return Ok(Outcome::fail(
899 FailureCode::Conflict,
900 format!("{branch} could not be updated: {reason}"),
901 ));
902 }
GitHub Actions on g1t, part one: reading workflows903 self.publish_push(
904 &target,
905 &format!("refs/heads/{branch}"),
906 old.as_deref(),
907 &new,
908 Some(a.actor.id),
909 )
Events service in Rust, with RFC 3339 times and accurate push events910 .await?;
Diffs on attempts; hosted agent presented as the g1t agent911 Ok(Outcome::Ok(Landed {
912 commit: new,
913 previous: old,
914 }))
915 }
916
917 async fn compare(&self, a: CompareArgs) -> Result<Outcome<Comparison>> {
918 let Some(repo) = self
Members can read a private repository's pull request forks919 .visible(self.registry.by_id(&a.repo_id).await?, &a.viewer)
Diffs on attempts; hosted agent presented as the g1t agent920 .await?
921 else {
922 return Ok(not_found());
923 };
924 let git = self.store.open(&store_key(&repo)).await?;
Pull requests from branches925 let head_ref = a.head.as_deref().unwrap_or(&repo.default_branch);
Agents as a team: lifecycle, merge queue, billing and a new shell926 // The head's history is only searched when the base is worked out
927 // from another branch.
928 let depth = if a.base.is_some() || is_commit_hash(head_ref) { 1 } else { MAX_ANCESTRY };
929 let history = git.log(head_ref, depth).await?;
Diffs on attempts; hosted agent presented as the g1t agent930 let Some(head) = history.first() else {
931 return Ok(Outcome::fail(
932 FailureCode::Conflict,
Pull requests from branches933 "There are no commits to compare.",
Diffs on attempts; hosted agent presented as the g1t agent934 ));
935 };
936
Pull requests from branches937 // Where the head's history meets the default branch of `against`.
938 let shared_with = async |against: &Repo| -> Result<Option<String>> {
939 let against_git = self.store.open(&store_key(against)).await?;
940 let shared: HashSet<String> = against_git
941 .log(&against.default_branch, MAX_ANCESTRY)
942 .await?
943 .into_iter()
944 .map(|commit| commit.hash)
945 .collect();
946 nearest_ancestor_in(&git, &history, &shared).await
947 };
Diffs on attempts; hosted agent presented as the g1t agent948 let base = match (a.base, &repo.fork_of) {
949 (Some(base), _) => Some(base),
950 // A fork is compared with the last commit it shares with the
951 // repository it came from.
952 (None, Some(target_id)) => match self.registry.by_id(target_id).await? {
Pull requests from branches953 Some(target) => shared_with(&target).await?,
Diffs on attempts; hosted agent presented as the g1t agent954 None => None,
955 },
Pull requests from branches956 // A branch, with the point where it left the default branch.
Agents as a team: lifecycle, merge queue, billing and a new shell957 // A single commit, with its first parent.
958 (None, None) if is_commit_hash(head_ref) => head.parents.first().cloned(),
Pull requests from branches959 (None, None) if head_ref != repo.default_branch => shared_with(&repo).await?,
Diffs on attempts; hosted agent presented as the g1t agent960 (None, None) => head.parents.first().cloned(),
961 };
962 let base_tree = match &base {
963 Some(base) => git
964 .log(base, 1)
965 .await?
966 .into_iter()
967 .next()
968 .map(|commit| commit.tree_hash),
969 None => None,
970 };
971 let (files, truncated) =
972 diff::compare_trees(&git, base_tree.as_deref(), &head.tree_hash).await?;
973 Ok(Outcome::Ok(Comparison {
974 base,
975 head: head.hash.clone(),
976 files,
977 truncated,
978 }))
Rust repos service with shipping; pull requests kept in the model979 }
980
GitHub Actions on g1t, part one: reading workflows981 /// Reports that `git_ref` of `repo` (a full ref) now points to `after`.
Events service in Rust, with RFC 3339 times and accurate push events982 async fn publish_push(
983 &self,
984 repo: &Repo,
GitHub Actions on g1t, part one: reading workflows985 git_ref: &str,
986 before: Option<&str>,
Events service in Rust, with RFC 3339 times and accurate push events987 after: &str,
988 actor: Option<String>,
989 ) -> Result<()> {
Rust repos service with shipping; pull requests kept in the model990 self.publish(NewEvent {
991 kind: "git.push",
992 source: SOURCE,
993 repo_id: Some(repo.id.clone()),
994 actor,
995 data: GitPush {
996 repo_id: repo.id.clone(),
GitHub Actions on g1t, part one: reading workflows997 git_ref: git_ref.to_owned(),
998 before: before.map(str::to_owned),
Rust repos service with shipping; pull requests kept in the model999 after: after.to_owned(),
GitHub Actions on g1t, part one: reading workflows1000 default_branch: git_ref.strip_prefix("refs/heads/")
1001 == Some(repo.default_branch.as_str()),
Rust repos service with shipping; pull requests kept in the model1002 },
1003 })
1004 .await
1005 }
1006
1007 /// Git over HTTPS.
1008 async fn git_http(&self, request: Request, env: &Env) -> Result<Response> {
1009 let Some(git) = git_http::parse(&request.url()?) else {
1010 return Response::error("Not found", 404);
1011 };
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1012 // A workspace that was renamed: git follows a redirect when it
1013 // first asks for refs, and uses the new address from then on.
1014 if self.registry.by_path(&git.path).await?.is_none()
1015 && let Some(location) = git_http::renamed(&request.url()?, &env.service("IDENTITY")?).await?
1016 {
1017 return git_http::moved(&location, request.method() == Method::Get);
1018 }
Rust repos service with shipping; pull requests kept in the model1019 let viewer = git_http::viewer(&request, &env.service("IDENTITY")?).await?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1020 // A run credential is checked against its grants, then acts as the
1021 // person it works for. See run_access.rs.
1022 let (request, viewer, audit) = match self.admit_git(request, &git, viewer).await? {
1023 run_access::Admitted::Go { request, viewer, entry } => (request, viewer, entry),
1024 run_access::Admitted::Refused(response) => return Ok(response),
1025 };
Rust repos service with shipping; pull requests kept in the model1026 let access = self
1027 .git_access(GitAccessArgs {
1028 path: git.path.clone(),
1029 viewer: viewer.clone(),
1030 service: git.service,
1031 })
1032 .await?;
1033 let access = match access {
1034 Outcome::Ok(access) => access,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1035 refused => {
1036 let response = git_http::refuse(refused)?;
1037 self.finish_git(audit, response.status_code(), None).await;
1038 return Ok(response);
1039 }
Rust repos service with shipping; pull requests kept in the model1040 };
Agents as a team: lifecycle, merge queue, billing and a new shell1041 // A protected default branch takes changes only from a merged pull
1042 // request, which lands without going through here.
1043 let protected = match self.registry.by_path(&git.path).await? {
1044 Some(repo) if repo.protected && repo.fork_of.is_none() => Some(repo.default_branch),
1045 _ => None,
1046 };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1047 // Push protection: a push that adds a secret is refused. See secret_scan.rs.
1048 let scan = async |body: &[u8]| self.protect(&git.path, viewer.as_ref(), body).await;
Agents as a team: lifecycle, merge queue, billing and a new shell1049 let forwarded =
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1050 match git_http::forward(request, &git, &access, protected.as_deref(), scan).await? {
Agents as a team: lifecycle, merge queue, billing and a new shell1051 git_http::Push::Forwarded(forwarded) => forwarded,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1052 git_http::Push::Refused(response) => {
1053 self.finish_git(audit, 403, Some("The push would change a protected branch.".to_owned())).await;
1054 return Ok(response);
1055 }
1056 git_http::Push::Blocked(response) => {
1057 self.finish_git(audit, 403, Some("The push adds a secret.".to_owned())).await;
1058 return Ok(response);
1059 }
Agents as a team: lifecycle, merge queue, billing and a new shell1060 };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1061 self.finish_git(audit, forwarded.response.status_code(), None).await;
Rust repos service with shipping; pull requests kept in the model1062
1063 // Artifacts' own push notifications are per repository, which does
Events service in Rust, with RFC 3339 times and accurate push events1064 // not fit a repo per pull request, so the front end reports pushes
1065 // itself: one event for each branch that moved.
1066 let accepted = forwarded.response.status_code() == 200 && !forwarded.pushed.is_empty();
1067 if accepted && let Some(repo) = self.registry.by_path(&git.path).await? {
1068 let stored = self.store.open(&store_key(&repo)).await?;
1069 let actor = viewer.map(|user: User| user.id);
GitHub Actions on g1t, part one: reading workflows1070 for pushed in &forwarded.pushed {
Events service in Rust, with RFC 3339 times and accurate push events1071 // The store can refuse one ref and accept another, so each
GitHub Actions on g1t, part one: reading workflows1072 // branch is checked against where it actually is. A tag the
1073 // store cannot read back is taken as pushed.
1074 let moved = match pushed.branch() {
1075 Some(branch) => stored
1076 .log(branch, 1)
1077 .await?
1078 .first()
1079 .is_some_and(|commit| commit.hash == pushed.after),
1080 None => stored.log(&pushed.git_ref, 1).await.map_or(true, |head| {
1081 head.first().is_none_or(|commit| commit.hash == pushed.after)
1082 }),
1083 };
1084 if moved {
1085 self.publish_push(
1086 &repo,
1087 &pushed.git_ref,
1088 pushed.before.as_deref(),
1089 &pushed.after,
1090 actor.clone(),
1091 )
1092 .await?;
Events service in Rust, with RFC 3339 times and accurate push events1093 }
Rust repos service with shipping; pull requests kept in the model1094 }
1095 }
Events service in Rust, with RFC 3339 times and accurate push events1096 Ok(forwarded.response)
Rust repos service with shipping; pull requests kept in the model1097 }
1098}
1099
1100#[event(fetch)]
1101async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
1102 let repos = Repos {
1103 registry: Registry { db: env.d1("DB")? },
1104 store: ArtifactsStore::new(&env)?,
Events service in Rust, with RFC 3339 times and accurate push events1105 events: env.service("EVENTS")?,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1106 security: env.service("SECURITY").ok(),
Rust repos service with shipping; pull requests kept in the model1107 };
1108 let Some(method) = rpc_method(&request) else {
1109 return repos.git_http(request, &env).await;
1110 };
1111 let body: serde_json::Value = request.json().await?;
1112
1113 match method.as_str() {
1114 "get" => reply(&repos.get(args(body)?).await?),
1115 "get_by_id" => reply(&repos.get_by_id(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1116 "readable" => {
1117 let a: ReadableArgs = args(body)?;
1118 reply(&repos.registry.readable(&a.ids, &a.viewer).await?)
1119 }
1120 "public_namespaces" => {
1121 let a: PublicNamespacesArgs = args(body)?;
1122 reply(&repos.registry.public_namespaces(&a.owner_id).await?)
1123 }
Automations: rules in .g1t/automations that act when something happens1124 "path_by_id" => {
1125 let a: PathByIdArgs = args(body)?;
1126 reply(
1127 &repos
1128 .registry
1129 .by_id(&a.id)
1130 .await?
1131 .filter(|repo| repo.fork_of.is_none())
1132 .map(|repo| RepoPath {
1133 namespace: repo.namespace,
1134 name: repo.name,
1135 }),
1136 )
1137 }
Rust repos service with shipping; pull requests kept in the model1138 "list" => {
1139 let a: ListArgs = args(body)?;
1140 reply(
1141 &repos
1142 .registry
Workspaces own repositories1143 .list(
1144 &a.viewer,
1145 a.query.as_deref(),
1146 a.namespace.as_deref(),
1147 a.member_only,
1148 )
Rust repos service with shipping; pull requests kept in the model1149 .await?,
1150 )
1151 }
1152 "create" => reply(&repos.create(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell1153 "update" => reply(&repos.update(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model1154 "tree" => reply(&repos.tree(args(body)?).await?),
1155 "blob" => reply(&repos.blob(args(body)?).await?),
1156 "log" => reply(&repos.log(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell1157 "blame" => reply(&repos.blame(args(body)?).await?),
Issues and pull requests replace intents and attempts1158 "fork_for_pull" => reply(&repos.fork_for_pull(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model1159 "git_access" => reply(&repos.git_access(args(body)?).await?),
Pull requests from branches1160 "branches" => reply(&repos.branches(args(body)?).await?),
1161 "head" => reply(&repos.head(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell1162 "behind" => reply(&repos.behind(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1163 "divergence" => reply(&repos.divergence(args(body)?).await?),
Rust repos service with shipping; pull requests kept in the model1164 "land" => reply(&repos.land(args(body)?).await?),
Catching up with main takes seconds when the two sides touched different files1165 "update_pull_branch" => reply(&repos.update_pull_branch(args(body)?).await?),
Merge queue: tested states are deleted once their entry leaves1166 "delete_branch" => reply(&repos.delete_branch(args(body)?).await?),
Diffs on attempts; hosted agent presented as the g1t agent1167 "compare" => reply(&repos.compare(args(body)?).await?),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1168 "scan_history" => reply(&repos.scan_history(args(body)?).await?),
1169 "find_lockfiles" => reply(&repos.find_lockfiles(args(body)?).await?),
Search across all of g1t, Explore, and a command palette1170 "list_files" => reply(&repos.list_files(args(body)?).await?),
1171 "changed_files" => reply(&repos.changed_files(args(body)?).await?),
1172 "read_blobs" => reply(&repos.read_blobs(args(body)?).await?),
1173 "all_ids" => {
1174 let a: AllIdsArgs = args(body)?;
1175 let limit = a.limit.clamp(1, 500);
1176 let ids = repos.registry.ids_after(a.after.as_deref(), limit).await?;
1177 let next = (ids.len() == limit as usize).then(|| ids.last().cloned()).flatten();
1178 reply(&IdPage { ids, next })
1179 }
Rust repos service with shipping; pull requests kept in the model1180 _ => Response::error("Unknown method", 404),
1181 }
1182}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1183
1184/// Events from the bus. Only a workspace's rename concerns this service:
1185/// its repositories move to the workspace's current slug, asked of identity
1186/// by id, so a repeated or late delivery lands in the same place. Their git
1187/// store keys stay as they were.
1188#[event(queue)]
1189async fn queue(batch: MessageBatch<Event>, env: Env, _ctx: Context) -> Result<()> {
1190 let registry = Registry { db: env.d1("DB")? };
1191 let identity = env.service("IDENTITY")?;
1192 for message in batch.messages()? {
1193 let event = message.body();
1194 if event.kind != "workspace.renamed" {
1195 continue;
1196 }
1197 let Ok(renamed) = serde_json::from_value::<WorkspaceRenamed>(event.data.clone()) else {
1198 worker::console_error!("workspace.renamed {} could not be read", event.id);
1199 continue;
1200 };
1201 let names: HashMap<String, String> = g1t_kit::call(
1202 &identity,
1203 "usernames",
1204 &g1t_contracts::identity::UsernamesArgs {
1205 ids: vec![renamed.workspace_id.clone()],
1206 },
1207 )
1208 .await?;
1209 let current = names
1210 .get(&renamed.workspace_id)
1211 .cloned()
1212 .unwrap_or_else(|| renamed.to.clone());
1213 let left = registry
1214 .rename_namespace(&renamed.stale_slugs(&current), &current)
1215 .await?;
1216 if left > 0 {
1217 worker::console_error!(
1218 "{left} repositories stayed under {} or {}: {current} already has repositories of the same names",
1219 renamed.from,
1220 renamed.to
1221 );
1222 }
1223 }
1224 Ok(())
1225}