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/registry.rs

212 lines6,745 bytesCodeBlame
1//! Repository metadata in D1.
2
3use g1t_contracts::Viewer;
4use g1t_contracts::repos::{Repo, RepoPath};
5use serde::Deserialize;
6use worker::wasm_bindgen::JsValue;
7use worker::{D1Database, Result};
8
9#[derive(Deserialize)]
10struct RepoRow {
11 id: String,
12 namespace: String,
13 name: String,
14 description: Option<String>,
15 is_private: u8,
16 owner_id: String,
17 default_branch: String,
18 fork_of: Option<String>,
19 protected: u8,
20 created_at: String,
21}
22
23impl From<RepoRow> for Repo {
24 fn from(row: RepoRow) -> Self {
25 Repo {
26 id: row.id,
27 namespace: row.namespace,
28 name: row.name,
29 description: row.description,
30 is_private: row.is_private != 0,
31 owner_id: row.owner_id,
32 default_branch: row.default_branch,
33 fork_of: row.fork_of,
34 protected: row.protected != 0,
35 created_at: row.created_at,
36 }
37 }
38}
39
40/// The key a repo is stored under in the git store.
41pub fn store_key(repo: &Repo) -> String {
42 format!("{}--{}", repo.namespace, repo.name)
43}
44
45/// Whether the viewer may read `repo`, going by the repository alone. A
46/// private pull request fork is also readable by whoever can read the
47/// repository it came from, which `Repos::may_read` checks.
48pub fn can_read(repo: &Repo, viewer: &Viewer) -> bool {
49 !repo.is_private || can_write(repo, viewer)
50}
51
52/// A repository belongs to its workspace, so any member may write to it. A
53/// pull request's fork belongs to whoever opened the pull request.
54pub fn can_write(repo: &Repo, viewer: &Viewer) -> bool {
55 viewer.as_ref().is_some_and(|user| {
56 if repo.fork_of.is_some() {
57 user.id == repo.owner_id
58 } else {
59 user.is_member(&repo.namespace)
60 }
61 })
62}
63
64fn optional(value: &Option<String>) -> JsValue {
65 value.as_deref().map_or(JsValue::NULL, JsValue::from)
66}
67
68pub struct Registry {
69 pub db: D1Database,
70}
71
72impl Registry {
73 pub async fn by_path(&self, path: &RepoPath) -> Result<Option<Repo>> {
74 Ok(self
75 .db
76 .prepare("SELECT * FROM repos WHERE namespace = ? AND name = ?")
77 .bind(&[
78 path.namespace.to_lowercase().into(),
79 path.name.to_lowercase().into(),
80 ])?
81 .first::<RepoRow>(None)
82 .await?
83 .map(Repo::from))
84 }
85
86 pub async fn update(
87 &self,
88 id: &str,
89 description: Option<&str>,
90 is_private: bool,
91 protected: bool,
92 ) -> Result<()> {
93 self.db
94 .prepare("UPDATE repos SET description = ?, is_private = ?, protected = ? WHERE id = ?")
95 .bind(&[
96 description.map_or(JsValue::NULL, JsValue::from),
97 u32::from(is_private).into(),
98 u32::from(protected).into(),
99 id.into(),
100 ])?
101 .run()
102 .await?;
103 Ok(())
104 }
105
106 pub async fn by_id(&self, id: &str) -> Result<Option<Repo>> {
107 Ok(self
108 .db
109 .prepare("SELECT * FROM repos WHERE id = ?")
110 .bind(&[id.into()])?
111 .first::<RepoRow>(None)
112 .await?
113 .map(Repo::from))
114 }
115
116 /// Repos the viewer may see, newest first. Excludes pull request forks.
117 /// With `member_only`, only repos in the viewer's own workspaces.
118 pub async fn list(
119 &self,
120 viewer: &Viewer,
121 query: Option<&str>,
122 namespace: Option<&str>,
123 member_only: bool,
124 ) -> Result<Vec<Repo>> {
125 let workspaces: Vec<&str> = viewer
126 .iter()
127 .flat_map(|user| &user.workspaces)
128 .map(|membership| membership.slug.as_str())
129 .collect();
130 // An empty IN list is not valid SQL, so a viewer in no workspace
131 // gets a name no workspace can have.
132 let mut params: Vec<JsValue> = if workspaces.is_empty() {
133 vec!["".into()]
134 } else {
135 workspaces.iter().map(|slug| JsValue::from(*slug)).collect()
136 };
137 let mine = format!("namespace IN ({})", vec!["?"; params.len()].join(", "));
138 let mut conditions = vec![
139 "fork_of IS NULL".to_owned(),
140 if member_only {
141 mine
142 } else {
143 format!("(is_private = 0 OR {mine})")
144 },
145 ];
146 if let Some(namespace) = namespace {
147 conditions.push("namespace = ?".to_owned());
148 params.push(namespace.to_lowercase().into());
149 }
150 if let Some(query) = query.map(str::trim).filter(|query| !query.is_empty()) {
151 conditions
152 .push("(name LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\')".to_owned());
153 // LIKE wildcards in the query are matched literally.
154 let escaped: String = query
155 .chars()
156 .flat_map(|c| match c {
157 '\\' | '%' | '_' => vec!['\\', c],
158 _ => vec![c],
159 })
160 .collect();
161 let pattern = format!("%{escaped}%");
162 params.push(pattern.as_str().into());
163 params.push(pattern.into());
164 }
165 let sql = format!(
166 "SELECT * FROM repos WHERE {} ORDER BY created_at DESC, id DESC LIMIT 50",
167 conditions.join(" AND ")
168 );
169 let rows = self
170 .db
171 .prepare(sql)
172 .bind(&params)?
173 .all()
174 .await?
175 .results::<RepoRow>()?;
176 Ok(rows.into_iter().map(Repo::from).collect())
177 }
178
179 /// Forgets a repository that could not be filled.
180 pub async fn remove(&self, id: &str) -> Result<()> {
181 self.db
182 .prepare("DELETE FROM repos WHERE id = ?")
183 .bind(&[id.into()])?
184 .run()
185 .await?;
186 Ok(())
187 }
188
189 pub async fn insert(&self, repo: &Repo) -> Result<()> {
190 self.db
191 .prepare(
192 "INSERT INTO repos
193 (id, namespace, name, description, is_private, owner_id,
194 default_branch, fork_of, created_at)
195 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
196 )
197 .bind(&[
198 repo.id.as_str().into(),
199 repo.namespace.as_str().into(),
200 repo.name.as_str().into(),
201 optional(&repo.description),
202 (repo.is_private as u8).into(),
203 repo.owner_id.as_str().into(),
204 repo.default_branch.as_str().into(),
205 optional(&repo.fork_of),
206 repo.created_at.as_str().into(),
207 ])?
208 .run()
209 .await?;
210 Ok(())
211 }
212}