JavaScript is disabled, refresh for a better experience. ambee/giterated

ambee/giterated

Git repository hosting, collaboration, and discovery for the Fediverse.

Add all the user request handling

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨e3bda14

⁨src/backend/git.rs⁩ - ⁨17923⁩ bytes
Raw
1 use anyhow::Error;
2 use async_trait::async_trait;
3 use futures_util::StreamExt;
4 use git2::ObjectType;
5 use sqlx::{Either, PgPool};
6 use std::path::{Path, PathBuf};
7 use thiserror::Error;
8
9 use crate::messages::ValidatedUserAuthenticated;
10
11 use crate::model::instance::Instance;
12 use crate::model::repository::{
13 Commit, Repository, RepositoryObjectType, RepositoryTreeEntry, RepositoryVisibility,
14 };
15 use crate::model::user::User;
16 use crate::{
17 messages::repository::{
18 CreateRepositoryRequest, CreateRepositoryResponse, RepositoryFileInspectRequest,
19 RepositoryFileInspectionResponse, RepositoryInfoRequest, RepositoryIssueLabelsRequest,
20 RepositoryIssueLabelsResponse, RepositoryIssuesCountRequest, RepositoryIssuesCountResponse,
21 RepositoryIssuesRequest, RepositoryIssuesResponse,
22 },
23 model::repository::RepositoryView,
24 };
25
26 use super::{IssuesBackend, RepositoryBackend};
27
28 // TODO: Handle this
29 //region database structures
30
31 /// Repository in the database
32 #[derive(Debug, sqlx::FromRow)]
33 pub struct GitRepository {
34 #[sqlx(try_from = "String")]
35 pub owner_user: User,
36 pub name: String,
37 pub description: Option<String>,
38 pub visibility: RepositoryVisibility,
39 pub default_branch: String,
40 }
41
42 impl GitRepository {
43 // Separate function because "Private" will be expanded later
44 /// Checks if the user is allowed to view this repository
45 pub fn can_user_view_repository(&self, user: Option<&User>) -> bool {
46 !(matches!(self.visibility, RepositoryVisibility::Private)
47 && self.owner_user.instance.url != user.map_or("", |user| user.instance.url.as_str())
48 && self.owner_user.username != user.map_or("", |user| user.username.as_str()))
49 }
50
51 // This is in it's own function because I assume I'll have to add logic to this later
52 pub fn open_git2_repository(
53 &self,
54 repository_directory: &str,
55 ) -> Result<git2::Repository, GitBackendError> {
56 match git2::Repository::open(format!(
57 "{}/{}/{}/{}",
58 repository_directory, self.owner_user.instance.url, self.owner_user.username, self.name
59 )) {
60 Ok(repository) => Ok(repository),
61 Err(err) => {
62 let err = GitBackendError::FailedOpeningFromDisk(err);
63 error!("Couldn't open a repository, this is bad! {:?}", err);
64
65 Err(err)
66 }
67 }
68 }
69 }
70
71 //endregion
72
73 #[derive(Error, Debug)]
74 pub enum GitBackendError {
75 #[error("Failed creating repository")]
76 FailedCreatingRepository(git2::Error),
77 #[error("Failed inserting into the database")]
78 FailedInsertingIntoDatabase(sqlx::Error),
79 #[error("Failed finding repository {owner_user:?}/{name:?}")]
80 RepositoryNotFound { owner_user: String, name: String },
81 #[error("Repository {owner_user:?}/{name:?} already exists")]
82 RepositoryAlreadyExists { owner_user: String, name: String },
83 #[error("Repository couldn't be deleted from the disk")]
84 CouldNotDeleteFromDisk(std::io::Error),
85 #[error("Failed deleting repository from database")]
86 FailedDeletingFromDatabase(sqlx::Error),
87 #[error("Failed opening repository on disk")]
88 FailedOpeningFromDisk(git2::Error),
89 #[error("Couldn't find ref with name `{0}`")]
90 RefNotFound(String),
91 #[error("Couldn't find path in repository `{0}`")]
92 PathNotFound(String),
93 #[error("Couldn't find commit for path `{0}`")]
94 LastCommitNotFound(String),
95 }
96
97 pub struct GitBackend {
98 pub pg_pool: PgPool,
99 pub repository_folder: String,
100 pub instance: Instance,
101 }
102
103 impl GitBackend {
104 pub fn new(
105 pg_pool: &PgPool,
106 repository_folder: &str,
107 instance: impl ToOwned<Owned = Instance>,
108 ) -> Self {
109 Self {
110 pg_pool: pg_pool.clone(),
111 repository_folder: repository_folder.to_string(),
112 instance: instance.to_owned(),
113 }
114 }
115
116 pub async fn find_by_owner_user_name(
117 &self,
118 user: &User,
119 repository_name: &str,
120 ) -> Result<GitRepository, GitBackendError> {
121 if let Ok(repository) = sqlx::query_as!(GitRepository,
122 r#"SELECT owner_user, name, description, visibility as "visibility: _", default_branch FROM repositories WHERE owner_user = $1 AND name = $2"#,
123 user.to_string(), repository_name)
124 .fetch_one(&self.pg_pool.clone())
125 .await {
126 Ok(repository)
127 } else {
128 Err(GitBackendError::RepositoryNotFound {
129 owner_user: user.to_string(),
130 name: repository_name.to_string(),
131 })
132 }
133 }
134
135 pub async fn delete_by_owner_user_name(
136 &self,
137 user: &User,
138 repository_name: &str,
139 ) -> Result<u64, GitBackendError> {
140 if let Err(err) = std::fs::remove_dir_all(PathBuf::from(format!(
141 "{}/{}/{}/{}",
142 self.repository_folder, user.instance.url, user.username, repository_name
143 ))) {
144 let err = GitBackendError::CouldNotDeleteFromDisk(err);
145 error!(
146 "Couldn't delete repository from disk, this is bad! {:?}",
147 err
148 );
149
150 return Err(err);
151 }
152
153 // Delete the repository from the database
154 match sqlx::query!(
155 "DELETE FROM repositories WHERE owner_user = $1 AND name = $2",
156 user.to_string(),
157 repository_name
158 )
159 .execute(&self.pg_pool.clone())
160 .await
161 {
162 Ok(deleted) => Ok(deleted.rows_affected()),
163 Err(err) => Err(GitBackendError::FailedDeletingFromDatabase(err)),
164 }
165 }
166
167 // TODO: Find where this fits
168 // TODO: Cache this and general repository tree and invalidate select files on push
169 // TODO: Find better and faster technique for this
170 pub fn get_last_commit_of_file(
171 path: &str,
172 git: &git2::Repository,
173 start_commit: &git2::Commit,
174 ) -> anyhow::Result<Commit> {
175 let mut revwalk = git.revwalk()?;
176 revwalk.set_sorting(git2::Sort::TIME)?;
177 revwalk.push(start_commit.id())?;
178
179 for oid in revwalk {
180 let oid = oid?;
181 let commit = git.find_commit(oid)?;
182
183 // Merge commits have 2 or more parents
184 // Commits with 0 parents are handled different because we can't diff against them
185 if commit.parent_count() == 0 {
186 return Ok(commit.into());
187 } else if commit.parent_count() == 1 {
188 let tree = commit.tree()?;
189 let last_tree = commit.parent(0)?.tree()?;
190
191 // Get the diff between the current tree and the last one
192 let diff = git.diff_tree_to_tree(Some(&last_tree), Some(&tree), None)?;
193
194 for dd in diff.deltas() {
195 // Get the path of the current file we're diffing against
196 let current_path = dd.new_file().path().unwrap();
197
198 // Path or directory
199 if current_path.eq(Path::new(&path)) || current_path.starts_with(path) {
200 return Ok(commit.into());
201 }
202 }
203 }
204 }
205
206 Err(GitBackendError::LastCommitNotFound(path.to_string()))?
207 }
208 }
209
210 #[async_trait]
211 impl RepositoryBackend for GitBackend {
212 async fn create_repository(
213 &mut self,
214 raw_request: &ValidatedUserAuthenticated<CreateRepositoryRequest>,
215 ) -> Result<CreateRepositoryResponse, Error> {
216 let request = raw_request.inner().await;
217
218 // let public_key = public_key(&Instance {
219 // url: String::from("giterated.dev"),
220 // })
221 // .await
222 // .unwrap();
223 //
224 // match raw_request.validate(public_key).await {
225 // Ok(_) => info!("Request was validated"),
226 // Err(err) => {
227 // error!("Failed to validate request: {:?}", err);
228 // panic!();
229 // }
230 // }
231 //
232 // info!("Request was valid!");
233
234 // Check if repository already exists in the database
235 if let Ok(repository) = self
236 .find_by_owner_user_name(&request.owner, &request.name)
237 .await
238 {
239 let err = GitBackendError::RepositoryAlreadyExists {
240 owner_user: repository.owner_user.to_string(),
241 name: repository.name,
242 };
243 error!("{:?}", err);
244
245 return Ok(CreateRepositoryResponse::Failed);
246 }
247
248 // Insert the repository into the database
249 let _ = match sqlx::query_as!(GitRepository,
250 r#"INSERT INTO repositories VALUES ($1, $2, $3, $4, $5) RETURNING owner_user, name, description, visibility as "visibility: _", default_branch"#,
251 request.owner.to_string(), request.name, request.description, request.visibility as _, "master")
252 .fetch_one(&self.pg_pool.clone())
253 .await {
254 Ok(repository) => repository,
255 Err(err) => {
256 let err = GitBackendError::FailedInsertingIntoDatabase(err);
257 error!("Failed inserting into the database! {:?}", err);
258
259 return Ok(CreateRepositoryResponse::Failed);
260 }
261 };
262
263 // Create bare (server side) repository on disk
264 match git2::Repository::init_bare(PathBuf::from(format!(
265 "{}/{}/{}/{}",
266 self.repository_folder,
267 request.owner.instance.url,
268 request.owner.username,
269 request.name
270 ))) {
271 Ok(_) => {
272 debug!(
273 "Created new repository with the name {}/{}/{}",
274 request.owner.instance.url, request.owner.username, request.name
275 );
276 Ok(CreateRepositoryResponse::Created)
277 }
278 Err(err) => {
279 let err = GitBackendError::FailedCreatingRepository(err);
280 error!("Failed creating repository on disk!? {:?}", err);
281
282 // Delete repository from database
283 if let Err(err) = self
284 .delete_by_owner_user_name(&request.owner, request.name.as_str())
285 .await
286 {
287 return Err(Box::new(err).into());
288 }
289
290 // ???
291 Ok(CreateRepositoryResponse::Failed)
292 //Err(Box::new(err))
293 }
294 }
295 }
296
297 async fn repository_info(
298 &mut self,
299 // TODO: Allow non-authenticated???
300 raw_request: &ValidatedUserAuthenticated<RepositoryInfoRequest>,
301 ) -> Result<RepositoryView, Error> {
302 let request = raw_request.inner().await;
303
304 let repository = match self
305 .find_by_owner_user_name(
306 // &request.owner.instance.url,
307 &request.repository.owner,
308 &request.repository.name,
309 )
310 .await
311 {
312 Ok(repository) => repository,
313 Err(err) => return Err(Box::new(err).into()),
314 };
315
316 if !repository.can_user_view_repository(Some(&raw_request.user)) {
317 return Err(Box::new(GitBackendError::RepositoryNotFound {
318 owner_user: request.repository.owner.to_string(),
319 name: request.repository.name.clone(),
320 })
321 .into());
322 }
323
324 let git = match repository.open_git2_repository(&self.repository_folder) {
325 Ok(git) => git,
326 Err(err) => return Err(Box::new(err).into()),
327 };
328
329 let rev_name = match &request.rev {
330 None => {
331 if let Ok(head) = git.head() {
332 head.name().unwrap().to_string()
333 } else {
334 // Nothing in database, render empty tree.
335 return Ok(RepositoryView {
336 name: repository.name,
337 owner: request.repository.owner.clone(),
338 description: repository.description,
339 visibility: repository.visibility,
340 default_branch: repository.default_branch,
341 latest_commit: None,
342 tree_rev: None,
343 tree: vec![],
344 });
345 }
346 }
347 Some(rev_name) => {
348 // Find the reference, otherwise return GitBackendError
349 match git
350 .find_reference(format!("refs/heads/{}", rev_name).as_str())
351 .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string()))
352 {
353 Ok(reference) => reference.name().unwrap().to_string(),
354 Err(err) => return Err(Box::new(err).into()),
355 }
356 }
357 };
358
359 // Get the git object as a commit
360 let rev = match git
361 .revparse_single(rev_name.as_str())
362 .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string()))
363 {
364 Ok(rev) => rev,
365 Err(err) => return Err(Box::new(err).into()),
366 };
367 let commit = rev.as_commit().unwrap();
368
369 // this is stupid
370 let mut current_path = rev_name.replace("refs/heads/", "");
371
372 // Get the commit tree
373 let git_tree = if let Some(path) = &request.path {
374 // Add it to our full path string
375 current_path.push_str(format!("/{}", path).as_str());
376 // Get the specified path, return an error if it wasn't found.
377 let entry = match commit
378 .tree()
379 .unwrap()
380 .get_path(&PathBuf::from(path))
381 .map_err(|_| GitBackendError::PathNotFound(path.to_string()))
382 {
383 Ok(entry) => entry,
384 Err(err) => return Err(Box::new(err).into()),
385 };
386 // Turn the entry into a git tree
387 entry.to_object(&git).unwrap().as_tree().unwrap().clone()
388 } else {
389 commit.tree().unwrap()
390 };
391
392 // Iterate over the git tree and collect it into our own tree types
393 let mut tree = git_tree
394 .iter()
395 .map(|entry| {
396 let object_type = match entry.kind().unwrap() {
397 ObjectType::Tree => RepositoryObjectType::Tree,
398 ObjectType::Blob => RepositoryObjectType::Blob,
399 _ => unreachable!(),
400 };
401 let mut tree_entry =
402 RepositoryTreeEntry::new(entry.name().unwrap(), object_type, entry.filemode());
403
404 if request.extra_metadata {
405 // Get the file size if It's a blob
406 let object = entry.to_object(&git).unwrap();
407 if let Some(blob) = object.as_blob() {
408 tree_entry.size = Some(blob.size());
409 }
410
411 // Could possibly be done better
412 let path = if let Some(path) = current_path.split_once('/') {
413 format!("{}/{}", path.1, entry.name().unwrap())
414 } else {
415 entry.name().unwrap().to_string()
416 };
417
418 // Get the last commit made to the entry
419 if let Ok(last_commit) =
420 GitBackend::get_last_commit_of_file(&path, &git, commit)
421 {
422 tree_entry.last_commit = Some(last_commit);
423 }
424 }
425
426 tree_entry
427 })
428 .collect::<Vec<RepositoryTreeEntry>>();
429
430 // Sort the tree alphabetically and with tree first
431 tree.sort_unstable_by_key(|entry| entry.name.to_lowercase());
432 tree.sort_unstable_by_key(|entry| {
433 std::cmp::Reverse(format!("{:?}", entry.object_type).to_lowercase())
434 });
435
436 Ok(RepositoryView {
437 name: repository.name,
438 owner: request.repository.owner.clone(),
439 description: repository.description,
440 visibility: repository.visibility,
441 default_branch: repository.default_branch,
442 latest_commit: None,
443 tree_rev: Some(rev_name),
444 tree,
445 })
446 }
447
448 fn repository_file_inspect(
449 &mut self,
450 _request: &ValidatedUserAuthenticated<RepositoryFileInspectRequest>,
451 ) -> Result<RepositoryFileInspectionResponse, Error> {
452 todo!()
453 }
454
455 async fn repositories_for_user(&mut self, user: &User) -> Result<Vec<Repository>, Error> {
456 let mut repositories = sqlx::query_as!(
457 GitRepository,
458 r#"SELECT visibility as "visibility: _", owner_user, name, description, default_branch FROM repositories WHERE owner_user = $1"#,
459 user.to_string()
460 )
461 .fetch_many(&self.pg_pool);
462
463 let mut result = vec![];
464
465 while let Some(Ok(Either::Right(repository))) = repositories.next().await {
466 result.push(Repository {
467 owner: repository.owner_user,
468 name: repository.name,
469 instance: self.instance.clone(),
470 });
471 }
472
473 Ok(result)
474 }
475 }
476
477 impl IssuesBackend for GitBackend {
478 fn issues_count(
479 &mut self,
480 _request: &ValidatedUserAuthenticated<RepositoryIssuesCountRequest>,
481 ) -> Result<RepositoryIssuesCountResponse, Error> {
482 todo!()
483 }
484
485 fn issue_labels(
486 &mut self,
487 _request: &ValidatedUserAuthenticated<RepositoryIssueLabelsRequest>,
488 ) -> Result<RepositoryIssueLabelsResponse, Error> {
489 todo!()
490 }
491
492 fn issues(
493 &mut self,
494 _request: &ValidatedUserAuthenticated<RepositoryIssuesRequest>,
495 ) -> Result<RepositoryIssuesResponse, Error> {
496 todo!()
497 }
498 }
499