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

ambee/giterated

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

Move towards having GitBackend split into files

Emilia - ⁨1⁩ year ago

parent: tbd commit: ⁨e55da0e

⁨giterated-daemon/src/backend/git/mod.rs⁩ - ⁨24156⁩ bytes
Raw
1 use anyhow::Error;
2 use async_trait::async_trait;
3
4 use git2::BranchType;
5 use giterated_models::instance::{Instance, RepositoryCreateRequest};
6
7 use giterated_models::object::Object;
8 use giterated_models::repository::{
9 AccessList, Commit, DefaultBranch, Description, IssueLabel, Repository, RepositoryBranch,
10 RepositoryBranchRequest, RepositoryBranchesRequest, RepositoryCommitBeforeRequest,
11 RepositoryCommitFromIdRequest, RepositoryDiff, RepositoryDiffPatchRequest,
12 RepositoryDiffRequest, RepositoryFile, RepositoryFileFromIdRequest,
13 RepositoryFileFromPathRequest, RepositoryFileInspectRequest, RepositoryIssue,
14 RepositoryIssueLabelsRequest, RepositoryIssuesCountRequest, RepositoryIssuesRequest,
15 RepositoryLastCommitOfFileRequest, RepositoryStatistics, RepositoryStatisticsRequest,
16 RepositoryTag, RepositoryTagsRequest, RepositoryTreeEntry, RepositoryVisibility, Visibility,
17 };
18
19 use giterated_models::user::User;
20
21 use giterated_stack::{AuthenticatedUser, GiteratedStack, OperationState, StackOperationState};
22
23 use sqlx::PgPool;
24 use std::ops::Deref;
25 use std::{path::PathBuf, sync::Arc};
26 use thiserror::Error;
27 use tokio::sync::OnceCell;
28
29 pub mod branches;
30 pub mod commit;
31 pub mod diff;
32 pub mod file;
33 pub mod tags;
34
35 use super::{IssuesBackend, RepositoryBackend};
36
37 //region database structures
38
39 /// Repository in the database
40 #[derive(Debug, sqlx::FromRow)]
41 pub struct GitRepository {
42 #[sqlx(try_from = "String")]
43 pub owner_user: User,
44 pub name: String,
45 pub description: Option<String>,
46 pub visibility: RepositoryVisibility,
47 pub default_branch: String,
48 }
49
50 impl GitRepository {
51 // Separate function because "Private" will be expanded later
52 /// Checks if the user is allowed to view this repository
53 pub async fn can_user_view_repository(
54 &self,
55 our_instance: &Instance,
56 user: &Option<AuthenticatedUser>,
57 stack: &GiteratedStack,
58 ) -> bool {
59 if matches!(self.visibility, RepositoryVisibility::Public) {
60 return true;
61 }
62
63 // User must exist for any further checks to pass
64 let user = match user {
65 Some(user) => user,
66 None => return false,
67 };
68
69 if *user.deref() == self.owner_user {
70 // owner can always view
71 return true;
72 }
73
74 if matches!(self.visibility, RepositoryVisibility::Private) {
75 // Check if the user can view\
76 let access_list = stack
77 .new_get_setting::<_, AccessList>(&Repository {
78 owner: self.owner_user.clone(),
79 name: self.name.clone(),
80 instance: our_instance.clone(),
81 })
82 .await
83 .unwrap();
84
85 access_list
86 .0
87 .iter()
88 .any(|access_list_user| access_list_user == user.deref())
89 } else {
90 false
91 }
92 }
93
94 // This is in it's own function because I assume I'll have to add logic to this later
95 pub fn open_git2_repository(
96 &self,
97 repository_directory: &str,
98 ) -> Result<git2::Repository, GitBackendError> {
99 match git2::Repository::open(format!(
100 "{}/{}/{}/{}",
101 repository_directory, self.owner_user.instance, self.owner_user.username, self.name
102 )) {
103 Ok(repository) => Ok(repository),
104 Err(err) => {
105 let err = GitBackendError::FailedOpeningFromDisk(err);
106 error!("Couldn't open a repository, this is bad! {:?}", err);
107
108 Err(err)
109 }
110 }
111 }
112 }
113
114 //endregion
115
116 #[derive(Error, Debug)]
117 pub enum GitBackendError {
118 #[error("Failed creating repository")]
119 FailedCreatingRepository(git2::Error),
120 #[error("Failed inserting into the database")]
121 FailedInsertingIntoDatabase(sqlx::Error),
122 #[error("Failed finding repository {owner_user:?}/{name:?}")]
123 RepositoryNotFound { owner_user: String, name: String },
124 #[error("Repository {owner_user:?}/{name:?} already exists")]
125 RepositoryAlreadyExists { owner_user: String, name: String },
126 #[error("Repository couldn't be deleted from the disk")]
127 CouldNotDeleteFromDisk(std::io::Error),
128 #[error("Failed deleting repository from database")]
129 FailedDeletingFromDatabase(sqlx::Error),
130 #[error("Failed opening repository on disk")]
131 FailedOpeningFromDisk(git2::Error),
132 #[error("Couldn't find ref with name `{0}`")]
133 RefNotFound(String),
134 #[error("Couldn't find repository head")]
135 HeadNotFound,
136 #[error("Couldn't find default repository branch")]
137 DefaultNotFound,
138 #[error("Couldn't find path in repository `{0}`")]
139 PathNotFound(String),
140 #[error("Couldn't find branch with name `{0}`")]
141 BranchNotFound(String),
142 #[error("Couldn't find commit for path `{0}`")]
143 LastCommitNotFound(String),
144 #[error("Object ID `{0}` is invalid")]
145 InvalidObjectId(String),
146 #[error("Blob with ID `{0}` not found")]
147 BlobNotFound(String),
148 #[error("Tree with ID `{0}` not found")]
149 TreeNotFound(String),
150 #[error("Commit with ID `{0}` not found")]
151 CommitNotFound(String),
152 #[error("Parent for commit with ID `{0}` not found")]
153 CommitParentNotFound(String),
154 #[error("Failed diffing tree with ID `{0}` to tree with ID `{1}`")]
155 FailedDiffing(String, String),
156 }
157
158 pub struct GitBackend {
159 pg_pool: PgPool,
160 repository_folder: String,
161 instance: Instance,
162 stack: Arc<OnceCell<GiteratedStack>>,
163 }
164
165 impl GitBackend {
166 pub fn new(
167 pg_pool: &PgPool,
168 repository_folder: &str,
169 instance: impl ToOwned<Owned = Instance>,
170 stack: Arc<OnceCell<GiteratedStack>>,
171 ) -> Self {
172 let instance = instance.to_owned();
173
174 Self {
175 pg_pool: pg_pool.clone(),
176 // We make sure there's no end slash
177 repository_folder: repository_folder.trim_end_matches(&['/', '\\']).to_string(),
178 instance,
179 stack,
180 }
181 }
182
183 pub async fn find_by_owner_user_name(
184 &self,
185 user: &User,
186 repository_name: &str,
187 ) -> Result<GitRepository, GitBackendError> {
188 // TODO: Patch for use with new GetValue system
189 if let Ok(repository) = sqlx::query_as!(GitRepository,
190 r#"SELECT owner_user, name, description, visibility as "visibility: _", default_branch FROM repositories WHERE owner_user = $1 AND name = $2"#,
191 user.to_string(), repository_name)
192 .fetch_one(&self.pg_pool.clone())
193 .await {
194 Ok(repository)
195 } else {
196 Err(GitBackendError::RepositoryNotFound {
197 owner_user: user.to_string(),
198 name: repository_name.to_string(),
199 })
200 }
201 }
202
203 pub async fn delete_by_owner_user_name(
204 &self,
205 user: &User,
206 repository_name: &str,
207 ) -> Result<u64, GitBackendError> {
208 if let Err(err) = std::fs::remove_dir_all(PathBuf::from(format!(
209 "{}/{}/{}/{}",
210 self.repository_folder, user.instance, user.username, repository_name
211 ))) {
212 let err = GitBackendError::CouldNotDeleteFromDisk(err);
213 error!(
214 "Couldn't delete repository from disk, this is bad! {:?}",
215 err
216 );
217
218 return Err(err);
219 }
220
221 // Delete the repository from the database
222 self.delete_from_database(user, repository_name).await
223 }
224
225 /// Deletes the repository from the database
226 pub async fn delete_from_database(
227 &self,
228 user: &User,
229 repository_name: &str,
230 ) -> Result<u64, GitBackendError> {
231 match sqlx::query!(
232 "DELETE FROM repositories WHERE owner_user = $1 AND name = $2",
233 user.to_string(),
234 repository_name
235 )
236 .execute(&self.pg_pool.clone())
237 .await
238 {
239 Ok(deleted) => Ok(deleted.rows_affected()),
240 Err(err) => Err(GitBackendError::FailedDeletingFromDatabase(err)),
241 }
242 }
243
244 pub async fn open_repository_and_check_permissions(
245 &self,
246 owner: &User,
247 name: &str,
248 requester: &Option<AuthenticatedUser>,
249 ) -> Result<git2::Repository, GitBackendError> {
250 let repository = match self
251 .find_by_owner_user_name(
252 // &request.owner.instance.url,
253 owner, name,
254 )
255 .await
256 {
257 Ok(repository) => repository,
258 Err(err) => return Err(err),
259 };
260
261 if let Some(requester) = requester {
262 if !repository
263 .can_user_view_repository(
264 &self.instance,
265 &Some(requester.clone()),
266 self.stack.get().unwrap(),
267 )
268 .await
269 {
270 return Err(GitBackendError::RepositoryNotFound {
271 owner_user: repository.owner_user.to_string(),
272 name: repository.name.clone(),
273 });
274 }
275 } else if matches!(repository.visibility, RepositoryVisibility::Private) {
276 // Unauthenticated users can never view private repositories
277
278 return Err(GitBackendError::RepositoryNotFound {
279 owner_user: repository.owner_user.to_string(),
280 name: repository.name.clone(),
281 });
282 }
283
284 match repository.open_git2_repository(&self.repository_folder) {
285 Ok(git) => Ok(git),
286 Err(err) => Err(err),
287 }
288 }
289
290 /// Attempts to get the oid in this order:
291 /// 1. Full refname (refname_to_id)
292 /// 2. Short branch name (find_branch)
293 /// 3. Other (revparse_single)
294 pub fn get_oid_from_reference(
295 git: &git2::Repository,
296 rev: Option<&str>,
297 default_branch: &DefaultBranch,
298 ) -> Result<git2::Oid, GitBackendError> {
299 // If the rev is None try and get the default branch instead
300 let rev = rev.unwrap_or(default_branch.0.as_str());
301
302 // TODO: This is far from ideal or speedy and would love for a better way to check this in the same order, but I can't find proper methods to do any of this.
303 trace!("Attempting to get ref with name {}", rev);
304
305 // Try getting it as a refname (refs/heads/name)
306 if let Ok(oid) = git.refname_to_id(rev) {
307 Ok(oid)
308 // Try finding it as a short branch name
309 } else if let Ok(branch) = git.find_branch(rev, BranchType::Local) {
310 // SHOULD be safe to unwrap
311 Ok(branch.get().target().unwrap())
312 // As last resort, try revparsing (will catch short oid and tags)
313 } else if let Ok(object) = git.revparse_single(rev) {
314 Ok(match object.kind() {
315 Some(git2::ObjectType::Tag) => {
316 if let Ok(commit) = object.peel_to_commit() {
317 commit.id()
318 } else {
319 object.id()
320 }
321 }
322 _ => object.id(),
323 })
324 } else {
325 Err(GitBackendError::RefNotFound(rev.to_string()))
326 }
327 }
328 }
329
330 #[async_trait(?Send)]
331 impl RepositoryBackend for GitBackend {
332 async fn create_repository(
333 &mut self,
334 _user: &AuthenticatedUser,
335 request: &RepositoryCreateRequest,
336 ) -> Result<Repository, GitBackendError> {
337 // Check if repository already exists in the database
338 if let Ok(repository) = self
339 .find_by_owner_user_name(&request.owner, &request.name)
340 .await
341 {
342 let err = GitBackendError::RepositoryAlreadyExists {
343 owner_user: repository.owner_user.to_string(),
344 name: repository.name,
345 };
346 error!("{:?}", err);
347
348 return Err(err);
349 }
350
351 // Insert the repository into the database
352 let _ = match sqlx::query_as!(GitRepository,
353 r#"INSERT INTO repositories VALUES ($1, $2, $3, $4, $5) RETURNING owner_user, name, description, visibility as "visibility: _", default_branch"#,
354 request.owner.to_string(), request.name, request.description, request.visibility as _, "master")
355 .fetch_one(&self.pg_pool.clone())
356 .await {
357 Ok(repository) => repository,
358 Err(err) => {
359 let err = GitBackendError::FailedInsertingIntoDatabase(err);
360 error!("Failed inserting into the database! {:?}", err);
361
362 return Err(err);
363 }
364 };
365
366 // Create bare (server side) repository on disk
367 match git2::Repository::init_bare(PathBuf::from(format!(
368 "{}/{}/{}/{}",
369 self.repository_folder, request.owner.instance, request.owner.username, request.name
370 ))) {
371 Ok(_) => {
372 debug!(
373 "Created new repository with the name {}/{}/{}",
374 request.owner.instance, request.owner.username, request.name
375 );
376
377 let stack = self.stack.get().unwrap();
378
379 let repository = Repository {
380 owner: request.owner.clone(),
381 name: request.name.clone(),
382 instance: request.instance.as_ref().unwrap_or(&self.instance).clone(),
383 };
384
385 stack
386 .write_setting(
387 &repository,
388 Description(request.description.clone().unwrap_or_default()),
389 )
390 .await
391 .unwrap();
392
393 stack
394 .write_setting(&repository, Visibility(request.visibility.clone()))
395 .await
396 .unwrap();
397
398 stack
399 .write_setting(&repository, DefaultBranch(request.default_branch.clone()))
400 .await
401 .unwrap();
402
403 Ok(repository)
404 }
405 Err(err) => {
406 let err = GitBackendError::FailedCreatingRepository(err);
407 error!("Failed creating repository on disk {:?}", err);
408
409 // Delete repository from database
410 self.delete_from_database(&request.owner, request.name.as_str())
411 .await?;
412
413 // ???
414 Err(err)
415 }
416 }
417 }
418
419 /// If the OID can't be found because there's no repository head, this will return an empty `Vec`.
420 async fn repository_file_inspect(
421 &mut self,
422 requester: &Option<AuthenticatedUser>,
423 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
424 OperationState(operation_state): OperationState<StackOperationState>,
425 request: &RepositoryFileInspectRequest,
426 ) -> Result<Vec<RepositoryTreeEntry>, Error> {
427 self.handle_repository_file_inspect(
428 requester,
429 repository_object,
430 OperationState(operation_state),
431 request,
432 )
433 .await
434 }
435
436 async fn repository_file_from_id(
437 &mut self,
438 requester: &Option<AuthenticatedUser>,
439 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
440 OperationState(operation_state): OperationState<StackOperationState>,
441 request: &RepositoryFileFromIdRequest,
442 ) -> Result<RepositoryFile, Error> {
443 self.handle_repository_file_from_id(
444 requester,
445 repository_object,
446 OperationState(operation_state),
447 request,
448 )
449 .await
450 }
451
452 async fn repository_file_from_path(
453 &mut self,
454 requester: &Option<AuthenticatedUser>,
455 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
456 OperationState(operation_state): OperationState<StackOperationState>,
457 request: &RepositoryFileFromPathRequest,
458 ) -> Result<(RepositoryFile, String), Error> {
459 self.handle_repository_file_from_path(
460 requester,
461 repository_object,
462 OperationState(operation_state),
463 request,
464 )
465 .await
466 }
467
468 async fn repository_commit_from_id(
469 &mut self,
470 requester: &Option<AuthenticatedUser>,
471 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
472 OperationState(operation_state): OperationState<StackOperationState>,
473 request: &RepositoryCommitFromIdRequest,
474 ) -> Result<Commit, Error> {
475 self.handle_repository_commit_from_id(
476 requester,
477 repository_object,
478 OperationState(operation_state),
479 request,
480 )
481 .await
482 }
483
484 async fn repository_last_commit_of_file(
485 &mut self,
486 requester: &Option<AuthenticatedUser>,
487 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
488 OperationState(operation_state): OperationState<StackOperationState>,
489 request: &RepositoryLastCommitOfFileRequest,
490 ) -> Result<Commit, Error> {
491 self.repository_last_commit_of_file(
492 requester,
493 repository_object,
494 OperationState(operation_state),
495 request,
496 )
497 .await
498 }
499
500 async fn repository_diff(
501 &mut self,
502 requester: &Option<AuthenticatedUser>,
503 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
504 OperationState(operation_state): OperationState<StackOperationState>,
505 request: &RepositoryDiffRequest,
506 ) -> Result<RepositoryDiff, Error> {
507 self.handle_repository_diff(
508 requester,
509 repository_object,
510 OperationState(operation_state),
511 request,
512 )
513 .await
514 }
515
516 async fn repository_diff_patch(
517 &mut self,
518 requester: &Option<AuthenticatedUser>,
519 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
520 OperationState(operation_state): OperationState<StackOperationState>,
521 request: &RepositoryDiffPatchRequest,
522 ) -> Result<String, Error> {
523 self.handle_repository_diff_patch(
524 requester,
525 repository_object,
526 OperationState(operation_state),
527 request,
528 )
529 .await
530 }
531
532 async fn repository_commit_before(
533 &mut self,
534 requester: &Option<AuthenticatedUser>,
535 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
536 OperationState(operation_state): OperationState<StackOperationState>,
537 request: &RepositoryCommitBeforeRequest,
538 ) -> Result<Commit, Error> {
539 self.handle_repository_commit_before(
540 requester,
541 repository_object,
542 OperationState(operation_state),
543 request,
544 )
545 .await
546 }
547
548 // TODO: See where this would need to go in terms of being split up into a different file
549 /// Returns zero for all statistics if an OID wasn't found
550 async fn repository_get_statistics(
551 &mut self,
552 requester: &Option<AuthenticatedUser>,
553 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
554 OperationState(operation_state): OperationState<StackOperationState>,
555 request: &RepositoryStatisticsRequest,
556 ) -> Result<RepositoryStatistics, Error> {
557 let repository = repository_object.object();
558 let git = self
559 .open_repository_and_check_permissions(&repository.owner, &repository.name, requester)
560 .await?;
561
562 let default_branch = repository_object
563 .get::<DefaultBranch>(&operation_state)
564 .await?;
565 let tree_id =
566 match Self::get_oid_from_reference(&git, request.rev.as_deref(), &default_branch) {
567 Ok(oid) => oid,
568 Err(_) => return Ok(RepositoryStatistics::default()),
569 };
570
571 // Count the amount of branches and tags
572 let mut branches = 0;
573 let mut tags = 0;
574 if let Ok(references) = git.references() {
575 for reference in references.flatten() {
576 if reference.is_branch() {
577 branches += 1;
578 } else if reference.is_tag() {
579 tags += 1;
580 }
581 }
582 }
583
584 // Attempt to get the commit from the oid
585 let commits = if let Ok(commit) = git.find_commit(tree_id) {
586 // Get the total commit count if we found the tree oid commit
587 GitBackend::get_total_commit_count(&git, &commit)?
588 } else {
589 0
590 };
591
592 Ok(RepositoryStatistics {
593 commits,
594 branches,
595 tags,
596 })
597 }
598
599 /// .0: List of branches filtering by passed requirements.
600 /// .1: Total amount of branches after being filtered
601 async fn repository_get_branches(
602 &mut self,
603 requester: &Option<AuthenticatedUser>,
604 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
605 OperationState(operation_state): OperationState<StackOperationState>,
606 request: &RepositoryBranchesRequest,
607 ) -> Result<(Vec<RepositoryBranch>, usize), Error> {
608 self.handle_repository_get_branches(
609 requester,
610 repository_object,
611 OperationState(operation_state),
612 request,
613 )
614 .await
615 }
616
617 async fn repository_get_branch(
618 &mut self,
619 requester: &Option<AuthenticatedUser>,
620 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
621 OperationState(operation_state): OperationState<StackOperationState>,
622 request: &RepositoryBranchRequest,
623 ) -> Result<RepositoryBranch, Error> {
624 self.handle_repository_get_branch(
625 requester,
626 repository_object,
627 OperationState(operation_state),
628 request,
629 )
630 .await
631 }
632
633 /// .0: List of tags in passed range
634 /// .1: Total amount of tags
635 async fn repository_get_tags(
636 &mut self,
637 requester: &Option<AuthenticatedUser>,
638 repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>,
639 OperationState(operation_state): OperationState<StackOperationState>,
640 request: &RepositoryTagsRequest,
641 ) -> Result<(Vec<RepositoryTag>, usize), Error> {
642 self.handle_repository_get_tags(
643 requester,
644 repository_object,
645 OperationState(operation_state),
646 request,
647 )
648 .await
649 }
650
651 async fn exists(
652 &mut self,
653 requester: &Option<AuthenticatedUser>,
654 repository: &Repository,
655 ) -> Result<bool, Error> {
656 if let Ok(repository) = self
657 .find_by_owner_user_name(&repository.owner.clone(), &repository.name)
658 .await
659 {
660 Ok(repository
661 .can_user_view_repository(&self.instance, requester, self.stack.get().unwrap())
662 .await)
663 } else {
664 Ok(false)
665 }
666 }
667 }
668
669 impl IssuesBackend for GitBackend {
670 fn issues_count(
671 &mut self,
672 _requester: &Option<AuthenticatedUser>,
673 _request: &RepositoryIssuesCountRequest,
674 ) -> Result<u64, Error> {
675 todo!()
676 }
677
678 fn issue_labels(
679 &mut self,
680 _requester: &Option<AuthenticatedUser>,
681 _request: &RepositoryIssueLabelsRequest,
682 ) -> Result<Vec<IssueLabel>, Error> {
683 todo!()
684 }
685
686 fn issues(
687 &mut self,
688 _requester: &Option<AuthenticatedUser>,
689 _request: &RepositoryIssuesRequest,
690 ) -> Result<Vec<RepositoryIssue>, Error> {
691 todo!()
692 }
693 }
694
695 #[allow(unused)]
696 #[derive(Debug, sqlx::FromRow)]
697 struct RepositoryMetadata {
698 pub repository: String,
699 pub name: String,
700 pub value: String,
701 }
702