Skip to main content

max / makenotwork

7.2 KB · 262 lines History Blame Raw
1 //! Git repository CRUD and lookup queries.
2
3 use chrono::{DateTime, Utc};
4 use sqlx::{FromRow, PgPool};
5
6 use super::models::DbGitRepo;
7 use super::{GitRepoId, ProjectId, UserId, Visibility};
8 use crate::error::Result;
9
10 /// A public repo joined with its owner's username, for the explore page.
11 #[derive(FromRow)]
12 pub struct PublicRepoWithOwner {
13 pub name: String,
14 pub description: String,
15 pub created_at: DateTime<Utc>,
16 pub owner_username: String,
17 }
18
19 /// Register a new git repository for a user (default visibility: public).
20 ///
21 /// Validates `name` against `validate_git_repo_name` as a defense-in-depth
22 /// backstop, the SSH dispatch path and HTTP smart-protocol path both call
23 /// this with names supplied by untrusted remote git clients.
24 #[tracing::instrument(skip_all)]
25 pub async fn create_repo(pool: &PgPool, user_id: UserId, name: &str) -> Result<DbGitRepo> {
26 crate::validation::validate_git_repo_name(name)?;
27 let repo = sqlx::query_as::<_, DbGitRepo>(
28 r"
29 INSERT INTO git_repos (user_id, name)
30 VALUES ($1, $2)
31 RETURNING *
32 ",
33 )
34 .bind(user_id)
35 .bind(name)
36 .fetch_one(pool)
37 .await?;
38
39 Ok(repo)
40 }
41
42 /// Register a new git repository with explicit visibility.
43 #[tracing::instrument(skip_all)]
44 pub async fn create_repo_with_visibility(
45 pool: &PgPool,
46 user_id: UserId,
47 name: &str,
48 visibility: Visibility,
49 ) -> Result<DbGitRepo> {
50 crate::validation::validate_git_repo_name(name)?;
51 let repo = sqlx::query_as::<_, DbGitRepo>(
52 r"
53 INSERT INTO git_repos (user_id, name, visibility)
54 VALUES ($1, $2, $3)
55 RETURNING *
56 ",
57 )
58 .bind(user_id)
59 .bind(name)
60 .bind(visibility)
61 .fetch_one(pool)
62 .await?;
63
64 Ok(repo)
65 }
66
67 /// Look up a repo by its primary key. Returns `None` if not found.
68 #[tracing::instrument(skip_all)]
69 pub async fn get_repo_by_id(pool: &PgPool, repo_id: GitRepoId) -> Result<Option<DbGitRepo>> {
70 let repo = sqlx::query_as::<_, DbGitRepo>("SELECT * FROM git_repos WHERE id = $1")
71 .bind(repo_id)
72 .fetch_optional(pool)
73 .await?;
74
75 Ok(repo)
76 }
77
78 /// Look up a repo by its owning user and bare name. Returns `None` if not found.
79 #[tracing::instrument(skip_all)]
80 pub async fn get_repo_by_user_and_name(
81 pool: &PgPool,
82 user_id: UserId,
83 name: &str,
84 ) -> Result<Option<DbGitRepo>> {
85 let repo =
86 sqlx::query_as::<_, DbGitRepo>("SELECT * FROM git_repos WHERE user_id = $1 AND name = $2")
87 .bind(user_id)
88 .bind(name)
89 .fetch_optional(pool)
90 .await?;
91
92 Ok(repo)
93 }
94
95 /// List all repos owned by a user, newest first.
96 #[tracing::instrument(skip_all)]
97 pub async fn get_repos_by_user(pool: &PgPool, user_id: UserId) -> Result<Vec<DbGitRepo>> {
98 let repos = sqlx::query_as::<_, DbGitRepo>(
99 "SELECT * FROM git_repos WHERE user_id = $1 ORDER BY created_at DESC LIMIT 500",
100 )
101 .bind(user_id)
102 .fetch_all(pool)
103 .await?;
104
105 Ok(repos)
106 }
107
108 /// List public repos owned by a user, newest first.
109 #[tracing::instrument(skip_all)]
110 pub async fn get_public_repos_by_user(pool: &PgPool, user_id: UserId) -> Result<Vec<DbGitRepo>> {
111 let repos = sqlx::query_as::<_, DbGitRepo>(
112 "SELECT * FROM git_repos WHERE user_id = $1 AND visibility = 'public' ORDER BY created_at DESC LIMIT 500",
113 )
114 .bind(user_id)
115 .fetch_all(pool)
116 .await?;
117
118 Ok(repos)
119 }
120
121 /// List all repos linked to a specific project.
122 #[tracing::instrument(skip_all)]
123 pub async fn get_repos_by_project(pool: &PgPool, project_id: ProjectId) -> Result<Vec<DbGitRepo>> {
124 let repos = sqlx::query_as::<_, DbGitRepo>(
125 "SELECT * FROM git_repos WHERE project_id = $1 ORDER BY name ASC",
126 )
127 .bind(project_id)
128 .fetch_all(pool)
129 .await?;
130
131 Ok(repos)
132 }
133
134 /// Link a repo to a project (sets `project_id`).
135 #[tracing::instrument(skip_all)]
136 pub async fn link_repo_to_project(
137 pool: &PgPool,
138 repo_id: GitRepoId,
139 project_id: ProjectId,
140 ) -> Result<()> {
141 sqlx::query("UPDATE git_repos SET project_id = $2 WHERE id = $1")
142 .bind(repo_id)
143 .bind(project_id)
144 .execute(pool)
145 .await?;
146
147 Ok(())
148 }
149
150 /// Unlink a repo from its project (sets `project_id = NULL`).
151 #[tracing::instrument(skip_all)]
152 pub async fn unlink_repo_from_project(pool: &PgPool, repo_id: GitRepoId) -> Result<()> {
153 sqlx::query("UPDATE git_repos SET project_id = NULL WHERE id = $1")
154 .bind(repo_id)
155 .execute(pool)
156 .await?;
157
158 Ok(())
159 }
160
161 /// Update the visibility of a repo.
162 #[tracing::instrument(skip_all)]
163 pub async fn update_visibility(
164 pool: &PgPool,
165 repo_id: GitRepoId,
166 visibility: Visibility,
167 ) -> Result<()> {
168 sqlx::query("UPDATE git_repos SET visibility = $2 WHERE id = $1")
169 .bind(repo_id)
170 .bind(visibility)
171 .execute(pool)
172 .await?;
173
174 Ok(())
175 }
176
177 /// Update description and visibility in one call.
178 #[tracing::instrument(skip_all)]
179 pub async fn update_repo_settings(
180 pool: &PgPool,
181 repo_id: GitRepoId,
182 description: &str,
183 visibility: Visibility,
184 ) -> Result<()> {
185 sqlx::query("UPDATE git_repos SET description = $2, visibility = $3 WHERE id = $1")
186 .bind(repo_id)
187 .bind(description)
188 .bind(visibility)
189 .execute(pool)
190 .await?;
191
192 Ok(())
193 }
194
195 /// Delete a repo from the database (leaves files on disk for safety).
196 #[tracing::instrument(skip_all)]
197 pub async fn delete_repo(pool: &PgPool, repo_id: GitRepoId) -> Result<()> {
198 sqlx::query("DELETE FROM git_repos WHERE id = $1")
199 .bind(repo_id)
200 .execute(pool)
201 .await?;
202
203 Ok(())
204 }
205
206 /// List all public repos across all users, newest first, with owner username.
207 #[tracing::instrument(skip_all)]
208 pub async fn get_all_public_repos(
209 pool: &PgPool,
210 limit: i64,
211 offset: i64,
212 ) -> Result<Vec<PublicRepoWithOwner>> {
213 let repos = sqlx::query_as::<_, PublicRepoWithOwner>(
214 r"
215 SELECT g.name, g.description, g.created_at, u.username AS owner_username
216 FROM git_repos g
217 JOIN users u ON u.id = g.user_id
218 WHERE g.visibility = 'public'
219 ORDER BY g.created_at DESC
220 LIMIT $1 OFFSET $2
221 ",
222 )
223 .bind(limit)
224 .bind(offset)
225 .fetch_all(pool)
226 .await?;
227
228 Ok(repos)
229 }
230
231 /// Count all public repos (for pagination).
232 #[tracing::instrument(skip_all)]
233 pub async fn count_all_public_repos(pool: &PgPool) -> Result<i64> {
234 let count: (i64,) =
235 sqlx::query_as("SELECT COUNT(*) FROM git_repos WHERE visibility = 'public'")
236 .fetch_one(pool)
237 .await?;
238
239 Ok(count.0)
240 }
241
242 /// Every repository on the platform, with its owner's username.
243 ///
244 /// Visibility-blind on purpose: the caller is an operator rebuilding a
245 /// server-side projection of what is on disk, not a page deciding what to show.
246 /// Nothing user-facing should reach for this.
247 #[tracing::instrument(skip_all)]
248 pub async fn all_repos_with_owner(pool: &PgPool) -> Result<Vec<(GitRepoId, String, String)>> {
249 let rows = sqlx::query_as::<_, (GitRepoId, String, String)>(
250 r"
251 SELECT g.id, u.username, g.name
252 FROM git_repos g
253 JOIN users u ON u.id = g.user_id
254 ORDER BY u.username, g.name
255 ",
256 )
257 .fetch_all(pool)
258 .await?;
259
260 Ok(rows)
261 }
262