use anyhow::Error; use async_trait::async_trait; use git2::BranchType; use giterated_models::instance::{Instance, RepositoryCreateRequest}; use giterated_models::object::Object; use giterated_models::repository::{ AccessList, Commit, DefaultBranch, Description, IssueLabel, Repository, RepositoryBranch, RepositoryBranchRequest, RepositoryBranchesRequest, RepositoryCommitBeforeRequest, RepositoryCommitFromIdRequest, RepositoryDiff, RepositoryDiffPatchRequest, RepositoryDiffRequest, RepositoryFile, RepositoryFileFromIdRequest, RepositoryFileFromPathRequest, RepositoryFileInspectRequest, RepositoryIssue, RepositoryIssueLabelsRequest, RepositoryIssuesCountRequest, RepositoryIssuesRequest, RepositoryLastCommitOfFileRequest, RepositoryStatistics, RepositoryStatisticsRequest, RepositoryTag, RepositoryTagRequest, RepositoryTagsRequest, RepositoryTreeEntry, RepositoryVisibility, Visibility, }; use giterated_models::user::User; use giterated_stack::{AuthenticatedUser, GiteratedStack, OperationState, StackOperationState}; use sqlx::PgPool; use std::ops::Deref; use std::{path::PathBuf, sync::Arc}; use thiserror::Error; use tokio::sync::OnceCell; pub mod branches; pub mod commit; pub mod diff; pub mod file; pub mod tags; use super::{IssuesBackend, RepositoryBackend}; //region database structures /// Repository in the database #[derive(Debug, sqlx::FromRow)] pub struct GitRepository { #[sqlx(try_from = "String")] pub owner_user: User, pub name: String, pub description: Option, pub visibility: RepositoryVisibility, pub default_branch: String, } impl GitRepository { // Separate function because "Private" will be expanded later /// Checks if the user is allowed to view this repository pub async fn can_user_view_repository( &self, our_instance: &Instance, user: &Option, stack: &GiteratedStack, ) -> bool { if matches!(self.visibility, RepositoryVisibility::Public) { return true; } // User must exist for any further checks to pass let user = match user { Some(user) => user, None => return false, }; if *user.deref() == self.owner_user { // owner can always view return true; } if matches!(self.visibility, RepositoryVisibility::Private) { // Check if the user can view\ let access_list = stack .new_get_setting::<_, AccessList>(&Repository { owner: self.owner_user.clone(), name: self.name.clone(), instance: our_instance.clone(), }) .await .unwrap(); access_list .0 .iter() .any(|access_list_user| access_list_user == user.deref()) } else { false } } // This is in it's own function because I assume I'll have to add logic to this later pub fn open_git2_repository( &self, repository_directory: &str, ) -> Result { match git2::Repository::open(format!( "{}/{}/{}/{}", repository_directory, self.owner_user.instance, self.owner_user.username, self.name )) { Ok(repository) => Ok(repository), Err(err) => { let err = GitBackendError::FailedOpeningFromDisk(err); error!("Couldn't open a repository, this is bad! {:?}", err); Err(err) } } } } //endregion #[derive(Error, Debug)] pub enum GitBackendError { #[error("Failed creating repository")] FailedCreatingRepository(git2::Error), #[error("Failed inserting into the database")] FailedInsertingIntoDatabase(sqlx::Error), #[error("Failed finding repository {owner_user:?}/{name:?}")] RepositoryNotFound { owner_user: String, name: String }, #[error("Repository {owner_user:?}/{name:?} already exists")] RepositoryAlreadyExists { owner_user: String, name: String }, #[error("Repository couldn't be deleted from the disk")] CouldNotDeleteFromDisk(std::io::Error), #[error("Failed deleting repository from database")] FailedDeletingFromDatabase(sqlx::Error), #[error("Failed opening repository on disk")] FailedOpeningFromDisk(git2::Error), #[error("Couldn't find ref with name `{0}`")] RefNotFound(String), #[error("Couldn't find repository head")] HeadNotFound, #[error("Couldn't find default repository branch")] DefaultNotFound, #[error("Couldn't find tag with name `{0}`")] TagNotFound(String), #[error("Couldn't find path in repository `{0}`")] PathNotFound(String), #[error("Couldn't find branch with name `{0}`")] BranchNotFound(String), #[error("Couldn't find commit for path `{0}`")] LastCommitNotFound(String), #[error("Object ID `{0}` is invalid")] InvalidObjectId(String), #[error("Blob with ID `{0}` not found")] BlobNotFound(String), #[error("Tree with ID `{0}` not found")] TreeNotFound(String), #[error("Commit with ID `{0}` not found")] CommitNotFound(String), #[error("Parent for commit with ID `{0}` not found")] CommitParentNotFound(String), #[error("Failed diffing tree with ID `{0}` to tree with ID `{1}`")] FailedDiffing(String, String), } /// Parses and trims a git message (commit, tag) into a summary and body. pub fn parse_trim_git_message(message: &str) -> (Option, Option) { // Iterate over the lines let mut lines = message .lines() .filter_map(|line| { if line.is_empty() { None } else { // Trim the whitespace for every line let mut whitespace_removed = String::with_capacity(line.len()); line.split_whitespace().for_each(|word| { if !whitespace_removed.is_empty() { whitespace_removed.push(' '); } whitespace_removed.push_str(word); }); Some(whitespace_removed) } }) .collect::>(); let summary = Some(lines.remove(0)); let body = if lines.is_empty() { None } else { Some(lines.join("\n")) }; (summary, body) } pub struct GitBackend { pg_pool: PgPool, repository_folder: String, instance: Instance, stack: Arc>, } impl GitBackend { pub fn new( pg_pool: &PgPool, repository_folder: &str, instance: impl ToOwned, stack: Arc>, ) -> Self { let instance = instance.to_owned(); Self { pg_pool: pg_pool.clone(), // We make sure there's no end slash repository_folder: repository_folder.trim_end_matches(&['/', '\\']).to_string(), instance, stack, } } pub async fn find_by_owner_user_name( &self, user: &User, repository_name: &str, ) -> Result { // TODO: Patch for use with new GetValue system if let Ok(repository) = sqlx::query_as!(GitRepository, r#"SELECT owner_user, name, description, visibility as "visibility: _", default_branch FROM repositories WHERE owner_user = $1 AND name = $2"#, user.to_string(), repository_name) .fetch_one(&self.pg_pool.clone()) .await { Ok(repository) } else { Err(GitBackendError::RepositoryNotFound { owner_user: user.to_string(), name: repository_name.to_string(), }) } } pub async fn delete_by_owner_user_name( &self, user: &User, repository_name: &str, ) -> Result { if let Err(err) = std::fs::remove_dir_all(PathBuf::from(format!( "{}/{}/{}/{}", self.repository_folder, user.instance, user.username, repository_name ))) { let err = GitBackendError::CouldNotDeleteFromDisk(err); error!( "Couldn't delete repository from disk, this is bad! {:?}", err ); return Err(err); } // Delete the repository from the database self.delete_from_database(user, repository_name).await } /// Deletes the repository from the database pub async fn delete_from_database( &self, user: &User, repository_name: &str, ) -> Result { match sqlx::query!( "DELETE FROM repositories WHERE owner_user = $1 AND name = $2", user.to_string(), repository_name ) .execute(&self.pg_pool.clone()) .await { Ok(deleted) => Ok(deleted.rows_affected()), Err(err) => Err(GitBackendError::FailedDeletingFromDatabase(err)), } } pub async fn open_repository_and_check_permissions( &self, owner: &User, name: &str, requester: &Option, ) -> Result { let repository = match self .find_by_owner_user_name( // &request.owner.instance.url, owner, name, ) .await { Ok(repository) => repository, Err(err) => return Err(err), }; if let Some(requester) = requester { if !repository .can_user_view_repository( &self.instance, &Some(requester.clone()), self.stack.get().unwrap(), ) .await { return Err(GitBackendError::RepositoryNotFound { owner_user: repository.owner_user.to_string(), name: repository.name.clone(), }); } } else if matches!(repository.visibility, RepositoryVisibility::Private) { // Unauthenticated users can never view private repositories return Err(GitBackendError::RepositoryNotFound { owner_user: repository.owner_user.to_string(), name: repository.name.clone(), }); } match repository.open_git2_repository(&self.repository_folder) { Ok(git) => Ok(git), Err(err) => Err(err), } } /// Attempts to get the oid in this order: /// 1. Full refname (refname_to_id) /// 2. Short branch name (find_branch) /// 3. Other (revparse_single) pub fn get_oid_from_reference( git: &git2::Repository, rev: Option<&str>, default_branch: &DefaultBranch, ) -> Result { // If the rev is None try and get the default branch instead let rev = rev.unwrap_or(default_branch.0.as_str()); // 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. trace!("Attempting to get ref with name {}", rev); // Try getting it as a refname (refs/heads/name) if let Ok(oid) = git.refname_to_id(rev) { Ok(oid) // Try finding it as a short branch name } else if let Ok(branch) = git.find_branch(rev, BranchType::Local) { // SHOULD be safe to unwrap Ok(branch.get().target().unwrap()) // As last resort, try revparsing (will catch short oid and tags) } else if let Ok(object) = git.revparse_single(rev) { Ok(match object.kind() { Some(git2::ObjectType::Tag) => { if let Ok(commit) = object.peel_to_commit() { commit.id() } else { object.id() } } _ => object.id(), }) } else { Err(GitBackendError::RefNotFound(rev.to_string())) } } } #[async_trait(?Send)] impl RepositoryBackend for GitBackend { async fn create_repository( &mut self, _user: &AuthenticatedUser, request: &RepositoryCreateRequest, ) -> Result { // Check if repository already exists in the database if let Ok(repository) = self .find_by_owner_user_name(&request.owner, &request.name) .await { let err = GitBackendError::RepositoryAlreadyExists { owner_user: repository.owner_user.to_string(), name: repository.name, }; error!("{:?}", err); return Err(err); } // Insert the repository into the database let _ = match sqlx::query_as!(GitRepository, r#"INSERT INTO repositories VALUES ($1, $2, $3, $4, $5) RETURNING owner_user, name, description, visibility as "visibility: _", default_branch"#, request.owner.to_string(), request.name, request.description, request.visibility as _, "master") .fetch_one(&self.pg_pool.clone()) .await { Ok(repository) => repository, Err(err) => { let err = GitBackendError::FailedInsertingIntoDatabase(err); error!("Failed inserting into the database! {:?}", err); return Err(err); } }; // Create bare (server side) repository on disk match git2::Repository::init_bare(PathBuf::from(format!( "{}/{}/{}/{}", self.repository_folder, request.owner.instance, request.owner.username, request.name ))) { Ok(_) => { debug!( "Created new repository with the name {}/{}/{}", request.owner.instance, request.owner.username, request.name ); let stack = self.stack.get().unwrap(); let repository = Repository { owner: request.owner.clone(), name: request.name.clone(), instance: request.instance.as_ref().unwrap_or(&self.instance).clone(), }; stack .write_setting( &repository, Description(request.description.clone().unwrap_or_default()), ) .await .unwrap(); stack .write_setting(&repository, Visibility(request.visibility.clone())) .await .unwrap(); stack .write_setting(&repository, DefaultBranch(request.default_branch.clone())) .await .unwrap(); Ok(repository) } Err(err) => { let err = GitBackendError::FailedCreatingRepository(err); error!("Failed creating repository on disk {:?}", err); // Delete repository from database self.delete_from_database(&request.owner, request.name.as_str()) .await?; // ??? Err(err) } } } /// If the OID can't be found because there's no repository head, this will return an empty `Vec`. async fn repository_file_inspect( &mut self, requester: &Option, repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, OperationState(operation_state): OperationState, request: &RepositoryFileInspectRequest, ) -> Result, Error> { self.handle_repository_file_inspect( requester, repository_object, OperationState(operation_state), request, ) .await } async fn repository_file_from_id( &mut self, requester: &Option, repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, OperationState(operation_state): OperationState, request: &RepositoryFileFromIdRequest, ) -> Result { self.handle_repository_file_from_id( requester, repository_object, OperationState(operation_state), request, ) .await } async fn repository_file_from_path( &mut self, requester: &Option, repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, OperationState(operation_state): OperationState, request: &RepositoryFileFromPathRequest, ) -> Result<(RepositoryFile, String), Error> { self.handle_repository_file_from_path( requester, repository_object, OperationState(operation_state), request, ) .await } async fn repository_commit_from_id( &mut self, requester: &Option, repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, OperationState(operation_state): OperationState, request: &RepositoryCommitFromIdRequest, ) -> Result { self.handle_repository_commit_from_id( requester, repository_object, OperationState(operation_state), request, ) .await } async fn repository_last_commit_of_file( &mut self, requester: &Option, repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, OperationState(operation_state): OperationState, request: &RepositoryLastCommitOfFileRequest, ) -> Result { self.handle_repository_last_commit_of_file( requester, repository_object, OperationState(operation_state), request, ) .await } async fn repository_diff( &mut self, requester: &Option, repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, OperationState(operation_state): OperationState, request: &RepositoryDiffRequest, ) -> Result { self.handle_repository_diff( requester, repository_object, OperationState(operation_state), request, ) .await } async fn repository_diff_patch( &mut self, requester: &Option, repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, OperationState(operation_state): OperationState, request: &RepositoryDiffPatchRequest, ) -> Result { self.handle_repository_diff_patch( requester, repository_object, OperationState(operation_state), request, ) .await } async fn repository_commit_before( &mut self, requester: &Option, repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, OperationState(operation_state): OperationState, request: &RepositoryCommitBeforeRequest, ) -> Result { self.handle_repository_commit_before( requester, repository_object, OperationState(operation_state), request, ) .await } // TODO: See where this would need to go in terms of being split up into a different file /// Returns zero for all statistics if an OID wasn't found async fn repository_get_statistics( &mut self, requester: &Option, repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, OperationState(operation_state): OperationState, request: &RepositoryStatisticsRequest, ) -> Result { let repository = repository_object.object(); let git = self .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) .await?; let default_branch = repository_object .get::(&operation_state) .await?; let tree_id = match Self::get_oid_from_reference(&git, request.rev.as_deref(), &default_branch) { Ok(oid) => oid, Err(_) => return Ok(RepositoryStatistics::default()), }; // Count the amount of branches and tags let mut branches = 0; let mut tags = 0; if let Ok(references) = git.references() { for reference in references.flatten() { if reference.is_branch() { branches += 1; } else if reference.is_tag() { tags += 1; } } } // Attempt to get the commit from the oid let commits = if let Ok(commit) = git.find_commit(tree_id) { // Get the total commit count if we found the tree oid commit GitBackend::get_total_commit_count(&git, &commit)? } else { 0 }; Ok(RepositoryStatistics { commits, branches, tags, }) } /// .0: List of branches filtering by passed requirements. /// .1: Total amount of branches after being filtered async fn repository_get_branches( &mut self, requester: &Option, repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, OperationState(operation_state): OperationState, request: &RepositoryBranchesRequest, ) -> Result<(Vec, usize), Error> { self.handle_repository_get_branches( requester, repository_object, OperationState(operation_state), request, ) .await } async fn repository_get_branch( &mut self, requester: &Option, repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, OperationState(operation_state): OperationState, request: &RepositoryBranchRequest, ) -> Result { self.handle_repository_get_branch( requester, repository_object, OperationState(operation_state), request, ) .await } /// .0: List of tags in passed range /// .1: Total amount of tags async fn repository_get_tags( &mut self, requester: &Option, repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, OperationState(operation_state): OperationState, request: &RepositoryTagsRequest, ) -> Result<(Vec, usize), Error> { self.handle_repository_get_tags( requester, repository_object, OperationState(operation_state), request, ) .await } async fn repository_get_tag( &mut self, requester: &Option, repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, OperationState(operation_state): OperationState, request: &RepositoryTagRequest, ) -> Result { self.handle_repository_get_tag( requester, repository_object, OperationState(operation_state), request, ) .await } async fn exists( &mut self, requester: &Option, repository: &Repository, ) -> Result { if let Ok(repository) = self .find_by_owner_user_name(&repository.owner.clone(), &repository.name) .await { Ok(repository .can_user_view_repository(&self.instance, requester, self.stack.get().unwrap()) .await) } else { Ok(false) } } } impl IssuesBackend for GitBackend { fn issues_count( &mut self, _requester: &Option, _request: &RepositoryIssuesCountRequest, ) -> Result { todo!() } fn issue_labels( &mut self, _requester: &Option, _request: &RepositoryIssueLabelsRequest, ) -> Result, Error> { todo!() } fn issues( &mut self, _requester: &Option, _request: &RepositoryIssuesRequest, ) -> Result, Error> { todo!() } } #[allow(unused)] #[derive(Debug, sqlx::FromRow)] struct RepositoryMetadata { pub repository: String, pub name: String, pub value: String, }