Skip to content
984 linesCodeBlameRaw

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.

A dusk palette and isometric line art, with the landing page rewritten around agents as a team1//! Repository metadata in D1.
2
Agents and memory, checks and conflicts, profiles, slug renames, custom domains3use std::cell::RefCell;
4use std::collections::HashMap;
5
A dusk palette and isometric line art, with the landing page rewritten around agents as a team6use g1t_contracts::Viewer;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look7use g1t_contracts::access::{self, Capability, RepoRole};
A dusk palette and isometric line art, with the landing page rewritten around agents as a team8use g1t_contracts::repos::{Repo, RepoPath};
9use serde::Deserialize;
10use worker::wasm_bindgen::JsValue;
11use worker::{D1Database, Result};
12
13#[derive(Deserialize)]
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look14pub(crate) struct RepoRow {
A dusk palette and isometric line art, with the landing page rewritten around agents as a team15 id: String,
16 namespace: String,
17 name: String,
18 description: Option<String>,
19 is_private: u8,
20 owner_id: String,
21 default_branch: String,
22 fork_of: Option<String>,
23 protected: u8,
24 created_at: String,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains25 /// Null only on rows written before the column existed and not yet
26 /// migrated; their key is the one worked out from the path.
27 #[serde(default)]
28 store: Option<String>,
Search across all of g1t, Explore, and a command palette29 /// JSON; absent on rows read before the column existed.
30 #[serde(default)]
31 topics: Option<String>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look32 #[serde(default)]
33 website: Option<String>,
34 #[serde(default)]
35 archived_at: Option<String>,
36 #[serde(default)]
37 deleted_at: Option<String>,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms38 /// Bumped by everything that changes the repository's refs; see
39 /// [`RefsState`]. Absent on rows read before the column existed.
40 #[serde(default)]
41 refs_version: Option<f64>,
42 #[serde(default)]
43 refs_open_until: Option<f64>,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily44 /// A pull request working copy whose git data was removed, and the
45 /// head it had (forks.rs). Absent before the columns existed.
46 #[serde(default)]
47 retired_at: Option<String>,
48 #[serde(default)]
49 retired_head: Option<String>,
Merge branch 'worktree-agent-a2013627e5ea4ab13'50 /// Until when writes wait, and why: a move between namespaces
51 /// (moves.rs). Absent before the columns existed.
52 #[serde(default)]
53 writes_paused_until: Option<f64>,
54 #[serde(default)]
55 writes_paused_for: Option<String>,
56}
57
58thread_local! {
59 /// Repositories whose writes wait, by id: until when, and why. Filled
60 /// whenever a row is read.
61 static PAUSED: RefCell<HashMap<String, (u64, String)>> = RefCell::new(HashMap::new());
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms62}
63
Merge branch 'worktree-agent-a2013627e5ea4ab13'64/// Records whether writes to the repository with this id wait, as its row says.
65pub fn note_paused(id: &str, until: Option<u64>, reason: Option<&str>) {
66 PAUSED.with(|paused| {
67 let mut paused = paused.borrow_mut();
68 match until {
69 Some(until) => {
70 paused.insert(id.to_owned(), (until, reason.unwrap_or("maintenance").to_owned()));
71 }
72 None => {
73 paused.remove(id);
74 }
75 }
76 });
77}
78
79/// Why writes to the repository with this id wait at `now`, if they do, as
80/// its row last read here said.
81pub fn paused(id: &str, now: u64) -> Option<String> {
82 PAUSED.with(|paused| paused.borrow().get(id).filter(|(until, _)| *until > now).map(|(_, reason)| reason.clone()))
83}
84
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms85/// Where a repository's refs stand, as its row last said: `version` goes up
86/// with every change g1t makes to them, so an answer that lists them (see
87/// refs_cache.rs) is kept under the version it was made at, and a change
88/// leaves it behind. Until `open_until` (milliseconds) a credential that
89/// can change them is out of g1t's hands, and nothing is kept.
90#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
91pub struct RefsState {
92 pub version: u64,
93 pub open_until: u64,
94}
95
96/// The newest [`RefsState`] this isolate has read or written, by
97/// repository id. A version only goes up, so an older read finishing late
98/// never takes a newer one back.
99#[derive(Default)]
100pub struct RefsStates {
101 states: HashMap<String, RefsState>,
A dusk palette and isometric line art, with the landing page rewritten around agents as a team102}
103
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms104impl RefsStates {
105 pub fn note(&mut self, id: &str, state: RefsState) {
106 let kept = self.states.entry(id.to_owned()).or_default();
107 kept.version = kept.version.max(state.version);
108 kept.open_until = kept.open_until.max(state.open_until);
109 }
110
111 pub fn get(&self, id: &str) -> Option<RefsState> {
112 self.states.get(id).copied()
113 }
114}
115
Agents and memory, checks and conflicts, profiles, slug renames, custom domains116thread_local! {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms117 static REFS: RefCell<RefsStates> = RefCell::new(RefsStates::default());
118}
119
120/// Where the refs of the repository with this id stand, as this isolate
121/// last read them; `None` before the column existed or before its row was
122/// read here.
123pub fn refs_state(id: &str) -> Option<RefsState> {
124 REFS.with(|refs| refs.borrow().get(id))
125}
126
127fn note_refs(id: &str, version: Option<f64>, open_until: Option<f64>) {
128 if let Some(version) = version {
129 let state = RefsState {
130 version: version as u64,
131 open_until: open_until.unwrap_or(0.0) as u64,
132 };
133 REFS.with(|refs| refs.borrow_mut().note(id, state));
134 }
135}
136
137thread_local! {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily138 /// Working copies whose git data was removed, by id, with the head
139 /// each had (forks.rs). Filled whenever a row is read.
140 static RETIRED: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new());
141}
142
143/// The head a removed working copy had, if the repository with this id is one.
144pub fn retired(id: &str) -> Option<String> {
145 RETIRED.with(|retired| retired.borrow().get(id).cloned())
146}
147
148/// Records whether the repository with this id is a removed working copy.
149pub fn note_retired(id: &str, head: Option<&str>) {
150 RETIRED.with(|retired| {
151 let mut retired = retired.borrow_mut();
152 match head {
153 Some(head) => {
154 retired.insert(id.to_owned(), head.to_owned());
155 }
156 None => {
157 retired.remove(id);
158 }
159 }
160 });
161}
162
163thread_local! {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains164 /// Store keys that differ from the one a repository's path gives: those
165 /// of repositories whose workspace was renamed after they were made.
166 /// Filled whenever a row is read or written, so every `Repo` this
Merge branch 'worktree-agent-a2013627e5ea4ab13'167 /// service holds has its key here. A key changes only when a move
168 /// between namespaces switches it (moves.rs), and every row read
169 /// after that brings the new one, so requests sharing the isolate can
170 /// share the map.
Agents and memory, checks and conflicts, profiles, slug renames, custom domains171 static MOVED: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new());
172}
173
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms174/// How long a fetch may go by a repository's row as it was read a moment
175/// ago: a clone is two or three requests in quick succession, and each
176/// would otherwise read the same row. Short enough that making a repository
177/// private, archiving or deleting it applies within seconds.
178pub const RECENT_MS: u64 = 5_000;
179
180/// Repositories read in the last [`RECENT_MS`], by path. Only rows that
181/// were found are kept, so a repository just made is never missed.
182#[derive(Default)]
183pub struct Recent {
184 rows: HashMap<(String, String), (Repo, u64)>,
185}
186
187impl Recent {
188 fn key(path: &RepoPath) -> (String, String) {
189 (path.namespace.to_lowercase(), path.name.to_lowercase())
190 }
191
192 pub fn get(&self, path: &RepoPath, now: u64) -> Option<Repo> {
193 self.rows
194 .get(&Self::key(path))
195 .filter(|(_, read)| now.saturating_sub(*read) < RECENT_MS)
196 .map(|(repo, _)| repo.clone())
197 }
198
199 pub fn keep(&mut self, path: &RepoPath, repo: &Repo, now: u64) {
200 self.rows.retain(|_, (_, read)| now.saturating_sub(*read) < RECENT_MS);
201 self.rows.insert(Self::key(path), (repo.clone(), now));
202 }
203}
204
205thread_local! {
206 static RECENT: RefCell<Recent> = RefCell::new(Recent::default());
207}
208
Agents and memory, checks and conflicts, profiles, slug renames, custom domains209/// The key a repository's path gives: what every repository was stored
210/// under before workspaces could be renamed.
211pub fn path_key(repo: &Repo) -> String {
212 format!("{}--{}", repo.namespace, repo.name)
213}
214
Merge branch 'worktree-agent-a2013627e5ea4ab13'215/// Records where a repository is stored, when its path does not say. A
216/// key changes when the repository moves between namespaces (moves.rs),
217/// so one that is the path's again is forgotten.
Agents and memory, checks and conflicts, profiles, slug renames, custom domains218pub fn remember_store(repo: &Repo, store: &str) {
Merge branch 'worktree-agent-a2013627e5ea4ab13'219 MOVED.with(|moved| {
220 let mut moved = moved.borrow_mut();
221 if store != path_key(repo) {
222 moved.insert(repo.id.clone(), store.to_owned());
223 } else {
224 moved.remove(&repo.id);
225 }
226 });
Agents and memory, checks and conflicts, profiles, slug renames, custom domains227}
228
A dusk palette and isometric line art, with the landing page rewritten around agents as a team229impl From<RepoRow> for Repo {
230 fn from(row: RepoRow) -> Self {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains231 let repo = Repo {
A dusk palette and isometric line art, with the landing page rewritten around agents as a team232 id: row.id,
233 namespace: row.namespace,
234 name: row.name,
235 description: row.description,
236 is_private: row.is_private != 0,
237 owner_id: row.owner_id,
238 default_branch: row.default_branch,
239 fork_of: row.fork_of,
240 protected: row.protected != 0,
241 created_at: row.created_at,
Search across all of g1t, Explore, and a command palette242 topics: row
243 .topics
244 .as_deref()
245 .and_then(|topics| serde_json::from_str(topics).ok())
246 .unwrap_or_default(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look247 website: row.website,
248 archived_at: row.archived_at,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains249 };
250 if let Some(store) = &row.store {
251 remember_store(&repo, store);
A dusk palette and isometric line art, with the landing page rewritten around agents as a team252 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms253 note_refs(&repo.id, row.refs_version, row.refs_open_until);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily254 note_retired(&repo.id, row.retired_at.as_ref().and(row.retired_head.as_deref()));
Merge branch 'worktree-agent-a2013627e5ea4ab13'255 note_paused(&repo.id, row.writes_paused_until.map(|until| until as u64), row.writes_paused_for.as_deref());
Agents and memory, checks and conflicts, profiles, slug renames, custom domains256 repo
A dusk palette and isometric line art, with the landing page rewritten around agents as a team257 }
258}
259
260/// The key a repo is stored under in the git store.
261pub fn store_key(repo: &Repo) -> String {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily262 let key = MOVED
Agents and memory, checks and conflicts, profiles, slug renames, custom domains263 .with(|moved| moved.borrow().get(&repo.id).cloned())
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily264 .unwrap_or_else(|| path_key(repo));
265 // Its interactions with the store are metered for its workspace.
266 crate::meters::note_owner(&key, &repo.namespace);
267 key
A dusk palette and isometric line art, with the landing page rewritten around agents as a team268}
269
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look270/// The viewer's role on `repo` (see `g1t_contracts::access`): ownership of
271/// its workspace, the workspace's base permission, a direct grant, or
272/// Read on a public repository. A pull request's fork is its author's to
273/// write; whoever can read the repository it came from can read it too,
274/// which `Repos::may_read` checks.
275pub fn role(repo: &Repo, viewer: &Viewer) -> Option<RepoRole> {
276 if repo.fork_of.is_some() {
277 let author = viewer.as_ref().is_some_and(|user| user.id == repo.owner_id);
278 return if author {
279 Some(RepoRole::Write)
280 } else if repo.is_private {
281 None
282 } else {
283 Some(RepoRole::Read)
284 };
285 }
286 access::permission(viewer.as_ref(), repo)
287}
288
289/// Whether the viewer may read `repo`, going by the repository alone.
A dusk palette and isometric line art, with the landing page rewritten around agents as a team290pub fn can_read(repo: &Repo, viewer: &Viewer) -> bool {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look291 role(repo, viewer).is_some()
A dusk palette and isometric line art, with the landing page rewritten around agents as a team292}
293
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look294/// Whether the viewer may push to `repo`: Write or higher, or the author
295/// of a pull request's fork.
A dusk palette and isometric line art, with the landing page rewritten around agents as a team296pub fn can_write(repo: &Repo, viewer: &Viewer) -> bool {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look297 can(repo, viewer, Capability::Push)
A dusk palette and isometric line art, with the landing page rewritten around agents as a team298}
299
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look300/// Whether the viewer may do `capability` in `repo`. A fork has only its
301/// author's Write.
302pub fn can(repo: &Repo, viewer: &Viewer, capability: Capability) -> bool {
303 if repo.fork_of.is_some() {
304 return role(repo, viewer).is_some_and(|role| access::allows(role, capability))
305 && !access::OWNER_ONLY.contains(&capability);
306 }
307 access::can(viewer.as_ref(), repo, capability)
308}
309
A dusk palette and isometric line art, with the landing page rewritten around agents as a team310fn optional(value: &Option<String>) -> JsValue {
311 value.as_deref().map_or(JsValue::NULL, JsValue::from)
312}
313
314pub struct Registry {
315 pub db: D1Database,
316}
317
318impl Registry {
319 pub async fn by_path(&self, path: &RepoPath) -> Result<Option<Repo>> {
320 Ok(self
321 .db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look322 .prepare("SELECT * FROM repos WHERE namespace = ? AND name = ? AND deleted_at IS NULL")
A dusk palette and isometric line art, with the landing page rewritten around agents as a team323 .bind(&[
324 path.namespace.to_lowercase().into(),
325 path.name.to_lowercase().into(),
326 ])?
327 .first::<RepoRow>(None)
328 .await?
329 .map(Repo::from))
330 }
331
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms332 /// The repository at `path`, as read in the last few seconds if it was
333 /// (see [`RECENT_MS`]). For fetches only: a push always reads the row.
334 pub async fn by_path_recent(&self, path: &RepoPath) -> Result<Option<Repo>> {
335 let now = g1t_kit::now_ms();
336 if let Some(repo) = RECENT.with(|recent| recent.borrow().get(path, now)) {
337 return Ok(Some(repo));
338 }
339 let found = self.by_path(path).await?;
340 if let Some(repo) = &found {
341 RECENT.with(|recent| recent.borrow_mut().keep(path, repo, now));
342 }
343 Ok(found)
344 }
345
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look346 /// Its details; who can see it changes with `set_private`.
A dusk palette and isometric line art, with the landing page rewritten around agents as a team347 pub async fn update(
348 &self,
349 id: &str,
350 description: Option<&str>,
351 protected: bool,
Search across all of g1t, Explore, and a command palette352 topics: &[String],
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look353 website: Option<&str>,
A dusk palette and isometric line art, with the landing page rewritten around agents as a team354 ) -> Result<()> {
355 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look356 .prepare("UPDATE repos SET description = ?, protected = ?, topics = ?, website = ? WHERE id = ?")
A dusk palette and isometric line art, with the landing page rewritten around agents as a team357 .bind(&[
358 description.map_or(JsValue::NULL, JsValue::from),
359 u32::from(protected).into(),
Search across all of g1t, Explore, and a command palette360 serde_json::to_string(topics)?.into(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look361 website.map_or(JsValue::NULL, JsValue::from),
A dusk palette and isometric line art, with the landing page rewritten around agents as a team362 id.into(),
363 ])?
364 .run()
365 .await?;
366 Ok(())
367 }
368
369 pub async fn by_id(&self, id: &str) -> Result<Option<Repo>> {
370 Ok(self
371 .db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look372 .prepare("SELECT * FROM repos WHERE id = ? AND deleted_at IS NULL")
A dusk palette and isometric line art, with the landing page rewritten around agents as a team373 .bind(&[id.into()])?
374 .first::<RepoRow>(None)
375 .await?
376 .map(Repo::from))
377 }
378
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look379 /// The repository at `path`, deleted or not: what holds the name.
380 pub async fn by_path_any(&self, path: &RepoPath) -> Result<Option<(Repo, Option<String>)>> {
381 Ok(self
382 .db
383 .prepare("SELECT * FROM repos WHERE namespace = ? AND name = ?")
384 .bind(&[
385 path.namespace.to_lowercase().into(),
386 path.name.to_lowercase().into(),
387 ])?
388 .first::<RepoRow>(None)
389 .await?
390 .map(|mut row| {
391 let deleted_at = row.deleted_at.take();
392 (Repo::from(row), deleted_at)
393 }))
394 }
395
A dusk palette and isometric line art, with the landing page rewritten around agents as a team396 /// Repos the viewer may see, newest first. Excludes pull request forks.
397 /// With `member_only`, only repos in the viewer's own workspaces.
398 pub async fn list(
399 &self,
400 viewer: &Viewer,
401 query: Option<&str>,
402 namespace: Option<&str>,
403 member_only: bool,
404 ) -> Result<Vec<Repo>> {
405 let workspaces: Vec<&str> = viewer
406 .iter()
407 .flat_map(|user| &user.workspaces)
408 .map(|membership| membership.slug.as_str())
409 .collect();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look410 // The workspaces whose private repositories the viewer reads all
411 // of (an owner, or a base permission other than none), and the
412 // repositories they were given a role on: see access.rs. A probe
413 // repository in each workspace stands for all of them.
414 let reading: Vec<&str> = viewer
415 .iter()
416 .flat_map(|user| {
417 user.workspaces.iter().filter(move |membership| {
418 let probe = access::RepoRef { id: "", namespace: &membership.slug, private: true };
419 access::granted(user, probe).is_some()
420 })
421 })
422 .map(|membership| membership.slug.as_str())
423 .collect();
Repository listing follows a fine-grained token's selection; job tokens never get workflow files424 let mut granted: Vec<&str> = viewer
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look425 .iter()
Repository listing follows a fine-grained token's selection; job tokens never get workflow files426 .flat_map(|user| {
427 let token = user.token.as_deref();
428 user.grants
429 .iter()
430 .filter(move |grant| token.is_none_or(|token| token.covers_repo(&grant.repo_id, &grant.workspace)))
431 })
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look432 .map(|grant| grant.repo_id.as_str())
433 .collect();
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers434 // A token's selected repositories, where its owner's
Repository listing follows a fine-grained token's selection; job tokens never get workflow files435 // membership reaches them.
436 if let Some(user) = viewer.as_ref()
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers437 && let Some(reach) = user.token.as_deref().and_then(|token| token.reach.as_ref())
Repository listing follows a fine-grained token's selection; job tokens never get workflow files438 && let Some(workspace) = reach.workspace.as_deref()
439 {
440 granted.extend(
441 reach
442 .repo_ids
443 .iter()
444 .filter(|id| access::granted(user, access::RepoRef { id, namespace: workspace, private: true }).is_some())
445 .map(String::as_str),
446 );
447 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look448 let mut params: Vec<JsValue> = vec![
449 serde_json::to_string(&reading)?.into(),
450 serde_json::to_string(&granted)?.into(),
451 ];
452 let private_ok = "(namespace IN (SELECT value FROM json_each(?)) OR id IN (SELECT value FROM json_each(?)))";
A dusk palette and isometric line art, with the landing page rewritten around agents as a team453 let mut conditions = vec![
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look454 "fork_of IS NULL AND deleted_at IS NULL".to_owned(),
455 format!("(is_private = 0 OR {private_ok})"),
A dusk palette and isometric line art, with the landing page rewritten around agents as a team456 ];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look457 if member_only {
458 conditions.push("namespace IN (SELECT value FROM json_each(?))".to_owned());
459 params.push(serde_json::to_string(&workspaces)?.into());
460 }
A dusk palette and isometric line art, with the landing page rewritten around agents as a team461 if let Some(namespace) = namespace {
462 conditions.push("namespace = ?".to_owned());
463 params.push(namespace.to_lowercase().into());
464 }
465 if let Some(query) = query.map(str::trim).filter(|query| !query.is_empty()) {
466 conditions
467 .push("(name LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\')".to_owned());
468 // LIKE wildcards in the query are matched literally.
469 let escaped: String = query
470 .chars()
471 .flat_map(|c| match c {
472 '\\' | '%' | '_' => vec!['\\', c],
473 _ => vec![c],
474 })
475 .collect();
476 let pattern = format!("%{escaped}%");
477 params.push(pattern.as_str().into());
478 params.push(pattern.into());
479 }
480 let sql = format!(
481 "SELECT * FROM repos WHERE {} ORDER BY created_at DESC, id DESC LIMIT 50",
482 conditions.join(" AND ")
483 );
484 let rows = self
485 .db
486 .prepare(sql)
487 .bind(&params)?
488 .all()
489 .await?
490 .results::<RepoRow>()?;
491 Ok(rows.into_iter().map(Repo::from).collect())
492 }
493
Agents and memory, checks and conflicts, profiles, slug renames, custom domains494 /// Of these ids, the repositories (not forks) the viewer may read.
495 pub async fn readable(&self, ids: &[String], viewer: &Viewer) -> Result<Vec<Repo>> {
496 let ids: Vec<&String> = ids.iter().take(g1t_contracts::repos::MAX_READABLE).collect();
497 if ids.is_empty() {
498 return Ok(Vec::new());
499 }
500 // One parameter however many ids: D1 binds at most 100.
501 let rows = self
502 .db
503 .prepare(
504 "SELECT * FROM repos
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look505 WHERE id IN (SELECT value FROM json_each(?)) AND fork_of IS NULL AND deleted_at IS NULL",
Agents and memory, checks and conflicts, profiles, slug renames, custom domains506 )
507 .bind(&[serde_json::to_string(&ids)?.into()])?
508 .all()
509 .await?
510 .results::<RepoRow>()?;
511 Ok(rows
512 .into_iter()
513 .map(Repo::from)
514 .filter(|repo| can_read(repo, viewer))
515 .collect())
516 }
517
518 /// The workspaces in which this account made a public repository.
519 pub async fn public_namespaces(&self, owner_id: &str) -> Result<Vec<String>> {
520 #[derive(Deserialize)]
521 struct Row {
522 namespace: String,
523 }
524 Ok(self
525 .db
526 .prepare(
527 "SELECT DISTINCT namespace FROM repos
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look528 WHERE owner_id = ? AND is_private = 0 AND fork_of IS NULL AND deleted_at IS NULL
Agents and memory, checks and conflicts, profiles, slug renames, custom domains529 ORDER BY namespace",
530 )
531 .bind(&[owner_id.into()])?
532 .all()
533 .await?
534 .results::<Row>()?
535 .into_iter()
536 .map(|row| row.namespace)
537 .collect())
538 }
539
Search across all of g1t, Explore, and a command palette540 /// Repositories that are not forks, by id, a page at a time.
Merge main (membership, two-factor, GitHub repo roles) into tokens541 /// Repositories that are not forks, with who created each, by id after
542 /// `after`.
543 pub async fn creators_after(&self, after: Option<&str>, limit: u32) -> Result<Vec<g1t_contracts::repos::RepoCreator>> {
544 self.db
545 .prepare(
546 "SELECT id, namespace, name, owner_id FROM repos
547 WHERE fork_of IS NULL AND deleted_at IS NULL AND id > ? ORDER BY id LIMIT ?",
548 )
549 .bind(&[after.unwrap_or("").into(), limit.into()])?
550 .all()
551 .await?
552 .results::<g1t_contracts::repos::RepoCreator>()
553 }
554
Search across all of g1t, Explore, and a command palette555 pub async fn ids_after(&self, after: Option<&str>, limit: u32) -> Result<Vec<String>> {
556 #[derive(Deserialize)]
557 struct Row {
558 id: String,
559 }
560 Ok(self
561 .db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look562 .prepare("SELECT id FROM repos WHERE fork_of IS NULL AND deleted_at IS NULL AND id > ? ORDER BY id LIMIT ?")
Search across all of g1t, Explore, and a command palette563 .bind(&[after.unwrap_or("").into(), limit.into()])?
564 .all()
565 .await?
566 .results::<Row>()?
567 .into_iter()
568 .map(|row| row.id)
569 .collect())
570 }
571
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put572 /// Adds a pushed pack's bytes to what the repository is counted as
573 /// holding: its own, or, for a pull request's working copy, the
574 /// repository it is a copy of, whose storage it is.
575 pub async fn add_stored_bytes(&self, repo: &Repo, bytes: u64) -> Result<()> {
576 if bytes == 0 {
577 return Ok(());
578 }
579 let root = repo.fork_of.as_deref().unwrap_or(&repo.id);
580 self.db
581 .prepare("UPDATE repos SET stored_bytes = stored_bytes + ? WHERE id = ?")
582 .bind(&[(bytes as f64).into(), root.into()])?
583 .run()
584 .await?;
585 Ok(())
586 }
587
588 /// Which of these `namespace/name` paths are private. A working copy
589 /// answers as its repository. Unknown paths are left out.
590 pub async fn visibility(&self, paths: &[String]) -> Result<Vec<g1t_contracts::repos::RepoVisibility>> {
591 let mut out = Vec::new();
592 for path in paths.iter().take(50) {
593 let Some((namespace, name)) = path.split_once('/') else { continue };
594 let Some(repo) = self
595 .by_path(&RepoPath { namespace: namespace.to_owned(), name: name.to_owned() })
596 .await?
597 else {
598 continue;
599 };
600 let is_private = match &repo.fork_of {
601 Some(parent) => self.by_id(parent).await?.map_or(repo.is_private, |parent| parent.is_private),
602 None => repo.is_private,
603 };
604 out.push(g1t_contracts::repos::RepoVisibility { path: path.clone(), is_private });
605 }
606 Ok(out)
607 }
608
609 /// What each workspace's repositories are counted as holding, private
610 /// and public apart. Working copies count toward their repository.
611 pub async fn storage(&self) -> Result<Vec<g1t_contracts::repos::WorkspaceStorage>> {
612 #[derive(Deserialize)]
613 struct Row {
614 namespace: String,
615 private_bytes: Option<f64>,
616 public_bytes: Option<f64>,
617 }
618 Ok(self
619 .db
620 .prepare(
621 "SELECT namespace,
622 SUM(CASE WHEN is_private = 1 THEN stored_bytes ELSE 0 END) AS private_bytes,
623 SUM(CASE WHEN is_private = 0 THEN stored_bytes ELSE 0 END) AS public_bytes
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look624 FROM repos WHERE fork_of IS NULL AND deleted_at IS NULL AND stored_bytes > 0 GROUP BY namespace",
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put625 )
626 .all()
627 .await?
628 .results::<Row>()?
629 .into_iter()
630 .map(|row| g1t_contracts::repos::WorkspaceStorage {
631 namespace: row.namespace,
632 private_bytes: row.private_bytes.unwrap_or(0.0) as i64,
633 public_bytes: row.public_bytes.unwrap_or(0.0) as i64,
634 })
635 .collect())
636 }
637
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look638 /// What one workspace's private repositories are counted as holding.
639 pub async fn private_bytes(&self, namespace: &str) -> Result<i64> {
640 #[derive(Deserialize)]
641 struct Row {
642 bytes: Option<f64>,
643 }
644 Ok(self
645 .db
646 .prepare("SELECT SUM(stored_bytes) AS bytes FROM repos WHERE namespace = ? AND is_private = 1 AND fork_of IS NULL AND deleted_at IS NULL")
647 .bind(&[namespace.into()])?
648 .first::<Row>(None)
649 .await?
650 .and_then(|row| row.bytes)
651 .unwrap_or(0.0) as i64)
652 }
653
A dusk palette and isometric line art, with the landing page rewritten around agents as a team654 /// Forgets a repository that could not be filled.
655 pub async fn remove(&self, id: &str) -> Result<()> {
656 self.db
657 .prepare("DELETE FROM repos WHERE id = ?")
658 .bind(&[id.into()])?
659 .run()
660 .await?;
661 Ok(())
662 }
663
Agents and memory, checks and conflicts, profiles, slug renames, custom domains664 /// Picks the store key for a repository about to be made, and
665 /// remembers it: the one its path gives, unless a repository already
666 /// holds that (one made in a workspace that has since been renamed,
667 /// whose old name this workspace now has), when its id.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily668 ///
669 /// `namespace` is the git store namespace it goes in (shards.rs), or
670 /// `None` for the default, `default`. A name is taken in any of them.
671 pub async fn claim_store_key(&self, repo: &Repo, namespace: Option<&str>, default: &str) -> Result<String> {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains672 let wanted = path_key(repo);
673 let held = self
674 .db
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily675 .prepare(
676 "SELECT 1 AS held FROM repos
Merge branch 'worktree-agent-a2013627e5ea4ab13'677 WHERE store = ?1 OR (instr(store, '/') > 0 AND substr(store, instr(store, '/') + 1) = ?1)
678 UNION ALL
679 SELECT 1 AS held FROM repo_move_copies WHERE name = ?1 AND cleaned_ms IS NULL",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily680 )
Agents and memory, checks and conflicts, profiles, slug renames, custom domains681 .bind(&[wanted.as_str().into()])?
682 .first::<serde_json::Value>(None)
683 .await?
684 .is_some();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily685 let name = if held { repo.id.clone() } else { wanted };
686 let key = crate::shards::compose(namespace, &name, default);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains687 remember_store(repo, &key);
688 Ok(key)
689 }
690
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily691
Agents and memory, checks and conflicts, profiles, slug renames, custom domains692 /// Moves a renamed workspace's repositories to its current slug, from
693 /// any of `stale`. A repository whose name the current slug already has
694 /// (one pushed there in the moment before this ran) stays where it is;
695 /// returns how many did.
696 pub async fn rename_namespace(&self, stale: &[String], current: &str) -> Result<usize> {
697 if stale.is_empty() {
698 return Ok(0);
699 }
700 let marks = vec!["?"; stale.len()].join(", ");
701 let mut moved: Vec<JsValue> = vec![current.into()];
702 moved.extend(stale.iter().map(|slug| JsValue::from(slug.as_str())));
703 let left: Vec<JsValue> = stale.iter().map(|slug| JsValue::from(slug.as_str())).collect();
704 let results = self
705 .db
706 .batch(vec![
707 self.db
708 .prepare(format!(
709 "UPDATE OR IGNORE repos SET namespace = ? WHERE namespace IN ({marks})"
710 ))
711 .bind(&moved)?,
712 self.db
713 .prepare(format!(
714 "SELECT count(*) AS left FROM repos WHERE namespace IN ({marks})"
715 ))
716 .bind(&left)?,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look717 // Git operations follow the workspace, added together.
718 self.db
719 .prepare(format!(
720 "INSERT INTO git_operations (namespace, hour, operations)
721 SELECT ?, hour, SUM(operations) FROM git_operations WHERE namespace IN ({marks}) GROUP BY hour
722 ON CONFLICT (namespace, hour) DO UPDATE SET operations = git_operations.operations + excluded.operations"
723 ))
724 .bind(&moved)?,
725 self.db
726 .prepare(format!("DELETE FROM git_operations WHERE namespace IN ({marks})"))
727 .bind(&left)?,
728 // Paths repositories were transferred away from follow the
729 // workspace too, so the old slug's redirect then finds them.
730 self.db
731 .prepare(format!(
732 "UPDATE OR IGNORE repo_redirects SET namespace = ? WHERE namespace IN ({marks})"
733 ))
734 .bind(&moved)?,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains735 ])
736 .await?;
737 #[derive(Deserialize)]
738 struct Left {
739 left: usize,
740 }
741 Ok(results
742 .get(1)
743 .map(|result| result.results::<Left>())
744 .transpose()?
745 .and_then(|rows| rows.into_iter().next())
746 .map_or(0, |row| row.left))
747 }
748
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms749 /// Records that the refs of the repository with this id changed, after
750 /// they did: what anything that lists them keeps goes stale.
751 pub async fn refs_moved(&self, id: &str) -> Result<()> {
752 self.bump_refs(
753 "UPDATE repos SET refs_version = refs_version + 1 WHERE id = ?
754 RETURNING refs_version, refs_open_until",
755 &[id.into()],
756 id,
757 )
758 .await
759 }
760
761 /// Records that a credential able to change the refs of the repository
762 /// with this id was handed out of g1t's hands, until `until`
763 /// (milliseconds): until then, nothing that lists them is kept.
764 pub async fn refs_open(&self, id: &str, until: u64) -> Result<()> {
765 self.bump_refs(
766 "UPDATE repos SET refs_version = refs_version + 1,
767 refs_open_until = max(coalesce(refs_open_until, 0), ?)
768 WHERE id = ? RETURNING refs_version, refs_open_until",
769 &[(until as f64).into(), id.into()],
770 id,
771 )
772 .await
773 }
774
775 async fn bump_refs(&self, sql: &str, params: &[JsValue], id: &str) -> Result<()> {
776 #[derive(Deserialize)]
777 struct Bumped {
778 refs_version: Option<f64>,
779 refs_open_until: Option<f64>,
780 }
781 let bumped = self
782 .db
783 .prepare(sql)
784 .bind(params)?
785 .first::<Bumped>(None)
786 .await?;
787 if let Some(bumped) = bumped {
788 note_refs(id, bumped.refs_version, bumped.refs_open_until);
789 }
790 Ok(())
791 }
792
A dusk palette and isometric line art, with the landing page rewritten around agents as a team793 pub async fn insert(&self, repo: &Repo) -> Result<()> {
794 self.db
795 .prepare(
796 "INSERT INTO repos
797 (id, namespace, name, description, is_private, owner_id,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains798 default_branch, fork_of, created_at, store)
799 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
A dusk palette and isometric line art, with the landing page rewritten around agents as a team800 )
801 .bind(&[
802 repo.id.as_str().into(),
803 repo.namespace.as_str().into(),
804 repo.name.as_str().into(),
805 optional(&repo.description),
806 (repo.is_private as u8).into(),
807 repo.owner_id.as_str().into(),
808 repo.default_branch.as_str().into(),
809 optional(&repo.fork_of),
810 repo.created_at.as_str().into(),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains811 store_key(repo).into(),
A dusk palette and isometric line art, with the landing page rewritten around agents as a team812 ])?
813 .run()
814 .await?;
815 Ok(())
816 }
817}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look818
819#[cfg(test)]
820mod tests {
821 use super::*;
822 use g1t_contracts::access::{BasePermission, RepoGrant};
823 use g1t_contracts::{Membership, Role, User};
824
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms825 #[test]
826 fn the_refs_state_kept_only_moves_forward() {
827 let mut states = RefsStates::default();
828 assert_eq!(states.get("rep_1"), None);
829 states.note("rep_1", RefsState { version: 3, open_until: 0 });
830 // A read that started before a bump and finished after it.
831 states.note("rep_1", RefsState { version: 2, open_until: 0 });
832 assert_eq!(states.get("rep_1").unwrap().version, 3);
833 states.note("rep_1", RefsState { version: 4, open_until: 9_000 });
834 states.note("rep_1", RefsState { version: 5, open_until: 0 });
835 assert_eq!(states.get("rep_1"), Some(RefsState { version: 5, open_until: 9_000 }));
836 assert_eq!(states.get("rep_2"), None);
837 }
838
839 #[test]
840 fn a_row_from_before_the_column_has_no_refs_state() {
841 let row = |version: Option<f64>| RepoRow {
842 id: format!("rep_row_{}", version.is_some()),
843 namespace: "acme".into(),
844 name: "rocket".into(),
845 description: None,
846 is_private: 0,
847 owner_id: "usr_owner".into(),
848 default_branch: "main".into(),
849 fork_of: None,
850 protected: 0,
851 created_at: String::new(),
852 store: None,
853 topics: None,
854 website: None,
855 archived_at: None,
856 deleted_at: None,
857 refs_version: version,
858 refs_open_until: None,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily859 retired_at: None,
860 retired_head: None,
Merge branch 'worktree-agent-a2013627e5ea4ab13'861 writes_paused_until: None,
862 writes_paused_for: None,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms863 };
864 let old = Repo::from(row(None));
865 assert_eq!(refs_state(&old.id), None);
866 let new = Repo::from(row(Some(7.0)));
867 assert_eq!(refs_state(&new.id), Some(RefsState { version: 7, open_until: 0 }));
868 }
869
870 #[test]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily871 fn a_removed_working_copy_is_known_by_its_row() {
872 note_retired("rep_fork", Some("abc"));
873 assert_eq!(retired("rep_fork").as_deref(), Some("abc"));
874 note_retired("rep_fork", None);
875 assert_eq!(retired("rep_fork"), None);
876 }
877
878 #[test]
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms879 fn a_repository_read_a_moment_ago_is_reused_for_a_few_seconds() {
880 let mut recent = Recent::default();
881 let path = RepoPath {
882 namespace: "Acme".into(),
883 name: "Rocket".into(),
884 };
885 recent.keep(&path, &repo(false), 1_000);
886 // Paths are matched as the table matches them, ignoring case.
887 let lower = RepoPath {
888 namespace: "acme".into(),
889 name: "rocket".into(),
890 };
891 assert_eq!(recent.get(&lower, 1_000 + RECENT_MS - 1).unwrap().id, "rep_1");
892 assert!(recent.get(&lower, 1_000 + RECENT_MS).is_none());
893 let other = RepoPath {
894 namespace: "acme".into(),
895 name: "booster".into(),
896 };
897 assert!(recent.get(&other, 1_000).is_none());
898 // Keeping another later drops the stale row.
899 recent.keep(&other, &repo(true), 1_000 + RECENT_MS);
900 assert_eq!(recent.rows.len(), 1);
901 }
902
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look903 fn repo(private: bool) -> Repo {
904 Repo {
905 id: "rep_1".into(),
906 namespace: "acme".into(),
907 name: "rocket".into(),
908 description: None,
909 is_private: private,
910 owner_id: "usr_owner".into(),
911 default_branch: "main".into(),
912 fork_of: None,
913 protected: false,
914 created_at: String::new(),
915 topics: Vec::new(),
916 website: None,
917 archived_at: None,
918 }
919 }
920
921 fn person(id: &str, memberships: Vec<Membership>, grants: Vec<(&str, RepoRole)>) -> Viewer {
922 Some(User {
923 id: id.into(),
924 username: id.into(),
925 verified: true,
926 workspaces: memberships,
927 grants: grants
928 .into_iter()
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar929 .map(|(repo_id, role)| RepoGrant { repo_id: repo_id.into(), workspace: "acme".into(), role, team: None })
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look930 .collect(),
931 ..User::default()
932 })
933 }
934
935 /// What git asks: clone and fetch need Read on a private repository,
936 /// push needs Write.
937 #[test]
938 fn git_reads_with_read_and_pushes_with_write() {
939 let private = repo(true);
940 let reader = person("usr_r", vec![], vec![("rep_1", RepoRole::Read)]);
941 assert!(can_read(&private, &reader));
942 assert!(!can_write(&private, &reader));
943 let writer = person("usr_w", vec![], vec![("rep_1", RepoRole::Write)]);
944 assert!(can_read(&private, &writer) && can_write(&private, &writer));
945 let stranger = person("usr_s", vec![], vec![("rep_2", RepoRole::Admin)]);
946 assert!(!can_read(&private, &stranger) && !can_write(&private, &stranger));
947 assert!(!can_read(&private, &None));
948 // A public repository: anyone clones, nobody without Write pushes.
949 let public = repo(false);
950 assert!(can_read(&public, &None) && !can_write(&public, &None));
951 assert!(can_read(&public, &stranger) && !can_write(&public, &stranger));
952 }
953
954 #[test]
955 fn members_follow_the_base_permission_and_owners_have_admin() {
956 let private = repo(true);
957 let default_member = person("usr_m", vec![Membership::member("acme")], vec![]);
958 assert!(can_write(&private, &default_member));
959 assert!(!can(&private, &default_member, Capability::ManageIntegrations));
960 let none = Membership { base_permission: Some(BasePermission::None), ..Membership::member("acme") };
961 let locked_out = person("usr_n", vec![none.clone()], vec![]);
962 assert!(!can_read(&private, &locked_out));
963 let given = person("usr_g", vec![none], vec![("rep_1", RepoRole::Triage)]);
964 assert!(can_read(&private, &given) && !can_write(&private, &given));
965 let owner = person("usr_o", vec![Membership { role: Role::Owner, ..Membership::member("acme") }], vec![]);
966 assert_eq!(role(&private, &owner), Some(RepoRole::Admin));
967 assert!(can(&private, &owner, Capability::Delete));
968 }
969
970 #[test]
971 fn a_pull_requests_fork_is_its_authors() {
972 let fork = Repo {
973 namespace: "pulls".into(),
974 fork_of: Some("rep_1".into()),
975 owner_id: "usr_a".into(),
976 ..repo(true)
977 };
978 let author = person("usr_a", vec![], vec![]);
979 assert!(can_write(&fork, &author));
980 assert!(!can(&fork, &author, Capability::ManageSettings));
981 let other = person("usr_b", vec![Membership::member("acme")], vec![]);
982 assert!(!can_write(&fork, &other));
983 }
984}

This file's history is long; its oldest lines are credited to the oldest commit read.