Skip to main content

max / makenotwork

6.5 KB · 248 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>(
71 "SELECT * FROM git_repos WHERE id = $1",
72 )
73 .bind(repo_id)
74 .fetch_optional(pool)
75 .await?;
76
77 Ok(repo)
78 }
79
80 /// Look up a repo by its owning user and bare name. Returns `None` if not found.
81 #[tracing::instrument(skip_all)]
82 pub async fn get_repo_by_user_and_name(
83 pool: &PgPool,
84 user_id: UserId,
85 name: &str,
86 ) -> Result<Option<DbGitRepo>> {
87 let repo = sqlx::query_as::<_, DbGitRepo>(
88 "SELECT * FROM git_repos WHERE user_id = $1 AND name = $2",
89 )
90 .bind(user_id)
91 .bind(name)
92 .fetch_optional(pool)
93 .await?;
94
95 Ok(repo)
96 }
97
98 /// List all repos owned by a user, newest first.
99 #[tracing::instrument(skip_all)]
100 pub async fn get_repos_by_user(pool: &PgPool, user_id: UserId) -> Result<Vec<DbGitRepo>> {
101 let repos = sqlx::query_as::<_, DbGitRepo>(
102 "SELECT * FROM git_repos WHERE user_id = $1 ORDER BY created_at DESC LIMIT 500",
103 )
104 .bind(user_id)
105 .fetch_all(pool)
106 .await?;
107
108 Ok(repos)
109 }
110
111 /// List public repos owned by a user, newest first.
112 #[tracing::instrument(skip_all)]
113 pub async fn get_public_repos_by_user(pool: &PgPool, user_id: UserId) -> Result<Vec<DbGitRepo>> {
114 let repos = sqlx::query_as::<_, DbGitRepo>(
115 "SELECT * FROM git_repos WHERE user_id = $1 AND visibility = 'public' ORDER BY created_at DESC LIMIT 500",
116 )
117 .bind(user_id)
118 .fetch_all(pool)
119 .await?;
120
121 Ok(repos)
122 }
123
124 /// List all repos linked to a specific project.
125 #[tracing::instrument(skip_all)]
126 pub async fn get_repos_by_project(
127 pool: &PgPool,
128 project_id: ProjectId,
129 ) -> Result<Vec<DbGitRepo>> {
130 let repos = sqlx::query_as::<_, DbGitRepo>(
131 "SELECT * FROM git_repos WHERE project_id = $1 ORDER BY name ASC",
132 )
133 .bind(project_id)
134 .fetch_all(pool)
135 .await?;
136
137 Ok(repos)
138 }
139
140 /// Link a repo to a project (sets `project_id`).
141 #[tracing::instrument(skip_all)]
142 pub async fn link_repo_to_project(
143 pool: &PgPool,
144 repo_id: GitRepoId,
145 project_id: ProjectId,
146 ) -> Result<()> {
147 sqlx::query("UPDATE git_repos SET project_id = $2 WHERE id = $1")
148 .bind(repo_id)
149 .bind(project_id)
150 .execute(pool)
151 .await?;
152
153 Ok(())
154 }
155
156 /// Unlink a repo from its project (sets `project_id = NULL`).
157 #[tracing::instrument(skip_all)]
158 pub async fn unlink_repo_from_project(pool: &PgPool, repo_id: GitRepoId) -> Result<()> {
159 sqlx::query("UPDATE git_repos SET project_id = NULL WHERE id = $1")
160 .bind(repo_id)
161 .execute(pool)
162 .await?;
163
164 Ok(())
165 }
166
167 /// Update the visibility of a repo.
168 #[tracing::instrument(skip_all)]
169 pub async fn update_visibility(
170 pool: &PgPool,
171 repo_id: GitRepoId,
172 visibility: Visibility,
173 ) -> Result<()> {
174 sqlx::query("UPDATE git_repos SET visibility = $2 WHERE id = $1")
175 .bind(repo_id)
176 .bind(visibility)
177 .execute(pool)
178 .await?;
179
180 Ok(())
181 }
182
183 /// Update description and visibility in one call.
184 #[tracing::instrument(skip_all)]
185 pub async fn update_repo_settings(
186 pool: &PgPool,
187 repo_id: GitRepoId,
188 description: &str,
189 visibility: Visibility,
190 ) -> Result<()> {
191 sqlx::query("UPDATE git_repos SET description = $2, visibility = $3 WHERE id = $1")
192 .bind(repo_id)
193 .bind(description)
194 .bind(visibility)
195 .execute(pool)
196 .await?;
197
198 Ok(())
199 }
200
201 /// Delete a repo from the database (leaves files on disk for safety).
202 #[tracing::instrument(skip_all)]
203 pub async fn delete_repo(pool: &PgPool, repo_id: GitRepoId) -> Result<()> {
204 sqlx::query("DELETE FROM git_repos WHERE id = $1")
205 .bind(repo_id)
206 .execute(pool)
207 .await?;
208
209 Ok(())
210 }
211
212 /// List all public repos across all users, newest first, with owner username.
213 #[tracing::instrument(skip_all)]
214 pub async fn get_all_public_repos(
215 pool: &PgPool,
216 limit: i64,
217 offset: i64,
218 ) -> Result<Vec<PublicRepoWithOwner>> {
219 let repos = sqlx::query_as::<_, PublicRepoWithOwner>(
220 r#"
221 SELECT g.name, g.description, g.created_at, u.username AS owner_username
222 FROM git_repos g
223 JOIN users u ON u.id = g.user_id
224 WHERE g.visibility = 'public'
225 ORDER BY g.created_at DESC
226 LIMIT $1 OFFSET $2
227 "#,
228 )
229 .bind(limit)
230 .bind(offset)
231 .fetch_all(pool)
232 .await?;
233
234 Ok(repos)
235 }
236
237 /// Count all public repos (for pagination).
238 #[tracing::instrument(skip_all)]
239 pub async fn count_all_public_repos(pool: &PgPool) -> Result<i64> {
240 let count: (i64,) = sqlx::query_as(
241 "SELECT COUNT(*) FROM git_repos WHERE visibility = 'public'",
242 )
243 .fetch_one(pool)
244 .await?;
245
246 Ok(count.0)
247 }
248