Skip to content

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.sh
259 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.

Projects: when each was last pushed, and how active it is lately1//! Staff-only views of workspaces, for sudo.g1t.sh.
2//!
3//! These methods take no viewer and check no membership. Only sudo calls
4//! them, over its service binding, once it has verified a Cloudflare Access
5//! sign-in against its staff list; nothing a customer can reach forwards to
6//! them. They read, and never change, anything.
7//!
8//! Every workspace is listed: there is one kind, and a person's own space
9//! is simply a workspace with one member. A user with no workspace has
10//! nothing to list, as nothing can exist outside one.
11
12use std::collections::HashMap;
13
14use g1t_contracts::Role;
15use g1t_contracts::identity::*;
16use serde::Deserialize;
17use worker::Result;
18
19use crate::Identity;
20
21#[derive(Deserialize)]
22struct ListRow {
23 id: String,
24 slug: String,
25 name: String,
26 created_at: String,
27 member_count: u32,
28}
29
30#[derive(Deserialize)]
31struct OwnerRow {
32 workspace_id: String,
33 username: String,
34 email: Option<String>,
35}
36
37#[derive(Deserialize)]
38struct DetailRow {
39 id: String,
40 slug: String,
41 name: String,
42 description: Option<String>,
43 created_at: String,
44 #[serde(default)]
45 protected: u8,
46}
47
48#[derive(Deserialize)]
49struct MemberRow {
50 username: String,
51 email: Option<String>,
52 role: Role,
53 joined: String,
54}
55
56/// A `LIKE` pattern matching `query` anywhere, lowercased, with `%`, `_`
57/// and the escape character itself taken literally. None for a blank query.
58pub(crate) fn like_pattern(query: Option<&str>) -> Option<String> {
59 let query = query.map(str::trim).filter(|q| !q.is_empty())?;
60 let mut pattern = String::from("%");
61 for c in query.to_lowercase().chars().take(100) {
62 if matches!(c, '%' | '_' | '\\') {
63 pattern.push('\\');
64 }
65 pattern.push(c);
66 }
67 pattern.push('%');
68 Some(pattern)
69}
70
71/// The workspaces, in their order, each with its owners.
72fn with_owners(rows: Vec<ListRow>, owners: Vec<OwnerRow>) -> Vec<AdminWorkspace> {
73 let mut by_workspace: HashMap<String, Vec<AdminOwner>> = HashMap::new();
74 for owner in owners {
75 by_workspace.entry(owner.workspace_id).or_default().push(AdminOwner {
76 username: owner.username,
77 email: owner.email,
78 });
79 }
80 rows.into_iter()
81 .map(|row| AdminWorkspace {
82 owners: by_workspace.remove(&row.id).unwrap_or_default(),
83 slug: row.slug,
84 name: row.name,
85 created_at: row.created_at,
86 member_count: row.member_count,
87 })
88 .collect()
89}
90
91impl Identity {
92 /// The workspaces staff can see, newest first. Staff only: see the
93 /// module's note.
94 /// Emails each owner of the workspace with a confirmed address. Only
95 /// other services call this; the link must stay on g1t.sh, so a notice
96 /// can never point owners anywhere else.
97 pub async fn notify_owners(&self, a: NotifyOwnersArgs) -> Result<u32> {
98 if !a.link.starts_with("https://g1t.sh/") {
99 return Ok(0);
100 }
101 #[derive(Deserialize)]
102 struct Owner {
103 email: Option<String>,
104 }
105 let owners = self
106 .db
107 .prepare(
108 "SELECT u.email FROM workspace_members m
109 JOIN users u ON u.id = m.user_id
110 JOIN workspaces w ON w.id = m.workspace_id
111 WHERE w.slug = ? AND w.deleted_at IS NULL AND m.role = 'owner' AND u.email_verified_at IS NOT NULL",
112 )
113 .bind(&[a.workspace.to_lowercase().into()])?
114 .all()
115 .await?
116 .results::<Owner>()?;
117 let mut sent = 0;
118 for email in owners.into_iter().filter_map(|owner| owner.email) {
119 match crate::email::send_link(&self.env, &email, &a.subject, &a.intro, &a.action, &a.link, &a.footer).await {
120 Ok(()) => sent += 1,
121 Err(error) => worker::console_error!("could not email an owner of {}: {error}", a.workspace),
122 }
123 }
124 Ok(sent)
125 }
126
127 pub async fn admin_workspaces(&self, a: AdminWorkspacesArgs) -> Result<Vec<AdminWorkspace>> {
128 let pattern = like_pattern(a.query.as_deref());
129 // The same filter picks the workspaces and, below, their owners.
130 // Deleted ones are listed apart (`admin_deleted_workspaces`).
131 let filter = if pattern.is_some() {
132 "WHERE w.deleted_at IS NULL AND (w.slug LIKE ?1 ESCAPE '\\' OR lower(w.name) LIKE ?1 ESCAPE '\\'
133 OR EXISTS (SELECT 1 FROM workspace_members om JOIN users ou ON ou.id = om.user_id
134 WHERE om.workspace_id = w.id AND om.role = 'owner'
135 AND (lower(ou.username) LIKE ?1 ESCAPE '\\' OR lower(ou.email) LIKE ?1 ESCAPE '\\')))"
136 } else {
137 "WHERE w.deleted_at IS NULL"
138 };
139 let chosen = format!(
140 "SELECT w.id FROM workspaces w {filter}
141 ORDER BY w.created_at DESC, w.slug LIMIT {ADMIN_WORKSPACES_LIMIT}"
142 );
143 let binds: Vec<worker::wasm_bindgen::JsValue> = pattern.into_iter().map(Into::into).collect();
144 let rows = self
145 .db
146 .prepare(format!(
147 "SELECT w.id, w.slug, w.name, w.created_at,
148 (SELECT count(*) FROM workspace_members m WHERE m.workspace_id = w.id) AS member_count
149 FROM workspaces w WHERE w.id IN ({chosen})
150 ORDER BY w.created_at DESC, w.slug"
151 ))
152 .bind(&binds)?
153 .all()
154 .await?
155 .results::<ListRow>()?;
156 let owners = self
157 .db
158 .prepare(format!(
159 "SELECT m.workspace_id, u.username, u.email FROM workspace_members m
160 JOIN users u ON u.id = m.user_id
161 WHERE m.role = 'owner' AND m.workspace_id IN ({chosen})
162 ORDER BY m.created_at, u.username"
163 ))
164 .bind(&binds)?
165 .all()
166 .await?
167 .results::<OwnerRow>()?;
168 Ok(with_owners(rows, owners))
169 }
170
171 /// One workspace with every member, or None. Staff only: see the
172 /// module's note.
173 pub async fn admin_workspace(&self, a: SlugArgs) -> Result<Option<AdminWorkspaceDetail>> {
174 let Some(row) = self
175 .db
176 .prepare("SELECT id, slug, name, description, created_at, protected FROM workspaces WHERE slug = ?")
177 .bind(&[a.slug.trim().to_lowercase().into()])?
178 .first::<DetailRow>(None)
179 .await?
180 else {
181 return Ok(None);
182 };
183 let members = self
184 .db
185 .prepare(
186 "SELECT u.username, u.email, m.role, m.created_at AS joined FROM workspace_members m
187 JOIN users u ON u.id = m.user_id
188 WHERE m.workspace_id = ?
189 ORDER BY m.role DESC, u.username",
190 )
191 .bind(&[row.id.as_str().into()])?
192 .all()
193 .await?
194 .results::<MemberRow>()?
195 .into_iter()
196 .map(|m| AdminMember {
197 username: m.username,
198 email: m.email,
199 role: m.role,
200 joined: m.joined,
201 })
202 .collect();
203 let protected = self.is_protected(&row.id, &row.slug, row.protected != 0).await?;
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.204 let suspended = self.workspace_suspension(&row.slug).await?;
Projects: when each was last pushed, and how active it is lately205 Ok(Some(AdminWorkspaceDetail {
206 protected,
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.207 suspended,
Projects: when each was last pushed, and how active it is lately208 slug: row.slug,
209 name: row.name,
210 description: row.description,
211 created_at: row.created_at,
212 members,
213 }))
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn a_blank_query_matches_everything() {
223 assert_eq!(like_pattern(None), None);
224 assert_eq!(like_pattern(Some(" ")), None);
225 }
226
227 #[test]
228 fn a_query_is_matched_literally_anywhere() {
229 assert_eq!(like_pattern(Some(" Acme ")).as_deref(), Some("%acme%"));
230 assert_eq!(like_pattern(Some("50%_off\\")).as_deref(), Some("%50\\%\\_off\\\\%"));
231 }
232
233 #[test]
234 fn owners_go_to_their_workspaces_in_order() {
235 let row = |id: &str, slug: &str| ListRow {
236 id: id.into(),
237 slug: slug.into(),
238 name: slug.into(),
239 created_at: "2026-10-04T00:00:00Z".into(),
240 member_count: 2,
241 };
242 let owner = |workspace_id: &str, username: &str| OwnerRow {
243 workspace_id: workspace_id.into(),
244 username: username.into(),
245 email: Some(format!("{username}@example.com")),
246 };
247 let listed = with_owners(
248 vec![row("wsp_2", "newer"), row("wsp_1", "older"), row("wsp_3", "orphan")],
249 vec![owner("wsp_1", "ada"), owner("wsp_2", "bob"), owner("wsp_1", "cy")],
250 );
251 let slugs: Vec<_> = listed.iter().map(|w| w.slug.as_str()).collect();
252 assert_eq!(slugs, ["newer", "older", "orphan"]);
253 let owners = |i: usize| listed[i].owners.iter().map(|o| o.username.as_str()).collect::<Vec<_>>();
254 assert_eq!(owners(0), ["bob"]);
255 assert_eq!(owners(1), ["ada", "cy"]);
256 assert!(owners(2).is_empty());
257 assert_eq!(listed[1].owners[0].email.as_deref(), Some("ada@example.com"));
258 }
259}

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