use anyhow::Error; use async_trait::async_trait; use futures_util::StreamExt; use giterated_models::{ model::{ instance::Instance, repository::{ Commit, IssueLabel, Repository, RepositoryIssue, RepositorySummary, RepositoryTreeEntry, RepositoryVisibility, }, settings::{AnySetting, RepositoryDescription, RepositoryVisibilitySetting, Setting}, user::{User, UserParseError}, }, operation::{ instance::RepositoryCreateRequest, repository::{ RepositoryFileInspectRequest, RepositoryIssueLabelsRequest, RepositoryIssuesCountRequest, RepositoryIssuesRequest, }, }, values::AnyValue, }; use serde_json::Value; use sqlx::{Either, PgPool}; use std::{ path::{Path, PathBuf}, sync::Arc, }; use thiserror::Error; use tokio::sync::Mutex; use super::{IssuesBackend, MetadataBackend, RepositoryBackend}; // TODO: Handle this //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 fn can_user_view_repository(&self, user: Option<&User>) -> bool { !matches!(self.visibility, RepositoryVisibility::Private) || (matches!(self.visibility, RepositoryVisibility::Private) && Some(&self.owner_user) == user) } // 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.url, 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 path in repository `{0}`")] PathNotFound(String), #[error("Couldn't find commit for path `{0}`")] LastCommitNotFound(String), } pub struct GitBackend { pub pg_pool: PgPool, pub repository_folder: String, pub instance: Instance, pub settings_provider: Arc>, } impl GitBackend { pub fn new( pg_pool: &PgPool, repository_folder: &str, instance: impl ToOwned, settings_provider: Arc>, ) -> Self { Self { pg_pool: pg_pool.clone(), repository_folder: repository_folder.to_string(), instance: instance.to_owned(), settings_provider, } } pub async fn find_by_owner_user_name( &self, user: &User, repository_name: &str, ) -> Result { 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.url, 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 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)), } } // TODO: Find where this fits // TODO: Cache this and general repository tree and invalidate select files on push // TODO: Find better and faster technique for this pub fn get_last_commit_of_file( path: &str, git: &git2::Repository, start_commit: &git2::Commit, ) -> anyhow::Result { let mut revwalk = git.revwalk()?; revwalk.set_sorting(git2::Sort::TIME)?; revwalk.push(start_commit.id())?; for oid in revwalk { let oid = oid?; let commit = git.find_commit(oid)?; // Merge commits have 2 or more parents // Commits with 0 parents are handled different because we can't diff against them if commit.parent_count() == 0 { return Ok(commit.into()); } else if commit.parent_count() == 1 { let tree = commit.tree()?; let last_tree = commit.parent(0)?.tree()?; // Get the diff between the current tree and the last one let diff = git.diff_tree_to_tree(Some(&last_tree), Some(&tree), None)?; for dd in diff.deltas() { // Get the path of the current file we're diffing against let current_path = dd.new_file().path().unwrap(); // Path or directory if current_path.eq(Path::new(&path)) || current_path.starts_with(path) { return Ok(commit.into()); } } } } Err(GitBackendError::LastCommitNotFound(path.to_string()))? } } #[async_trait] impl RepositoryBackend for GitBackend { async fn exists(&mut self, repository: &Repository) -> Result { if let Ok(_repository) = self .find_by_owner_user_name(&repository.owner, &repository.name) .await { return Ok(true); } else { return Ok(false); } } async fn create_repository( &mut self, _user: &User, 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.url, request.owner.username, request.name ))) { Ok(_) => { debug!( "Created new repository with the name {}/{}/{}", request.owner.instance.url, request.owner.username, request.name ); Ok(Repository { owner: request.owner.clone(), name: request.name.clone(), instance: request.instance.as_ref().unwrap_or(&self.instance).clone(), }) } Err(err) => { let err = GitBackendError::FailedCreatingRepository(err); error!("Failed creating repository on disk!? {:?}", err); // Delete repository from database self.delete_by_owner_user_name(&request.owner, request.name.as_str()) .await?; // ??? Err(err) } } } async fn get_value( &mut self, repository: &Repository, name: &str, ) -> Result, Error> { Ok(match name { "description" => unsafe { AnyValue::from_raw( self.get_setting(repository, RepositoryDescription::name()) .await? .0, ) }, "visibility" => unsafe { AnyValue::from_raw( self.get_setting(repository, RepositoryVisibilitySetting::name()) .await? .0, ) }, _ => { return Err(UserParseError.into()); } }) } async fn get_setting( &mut self, repository: &Repository, name: &str, ) -> Result { let mut provider = self.settings_provider.lock().await; Ok(provider.repository_get(repository, name).await?) } async fn write_setting( &mut self, repository: &Repository, name: &str, setting: &Value, ) -> Result<(), Error> { let mut provider = self.settings_provider.lock().await; provider .repository_write(repository, name, AnySetting(setting.clone())) .await } // async fn repository_info( // &mut self, // requester: Option<&User>, // request: &RepositoryInfoRequest, // ) -> Result { // let repository = match self // .find_by_owner_user_name( // // &request.owner.instance.url, // &request.repository.owner, // &request.repository.name, // ) // .await // { // Ok(repository) => repository, // Err(err) => return Err(Box::new(err).into()), // }; // if let Some(requester) = requester { // if !repository.can_user_view_repository(Some(&requester)) { // return Err(Box::new(GitBackendError::RepositoryNotFound { // owner_user: request.repository.owner.to_string(), // name: request.repository.name.clone(), // }) // .into()); // } // } else if matches!(repository.visibility, RepositoryVisibility::Private) { // info!("Unauthenticated"); // // Unauthenticated users can never view private repositories // return Err(Box::new(GitBackendError::RepositoryNotFound { // owner_user: request.repository.owner.to_string(), // name: request.repository.name.clone(), // }) // .into()); // } // let git = match repository.open_git2_repository(&self.repository_folder) { // Ok(git) => git, // Err(err) => return Err(Box::new(err).into()), // }; // let rev_name = match &request.rev { // None => { // if let Ok(head) = git.head() { // head.name().unwrap().to_string() // } else { // // Nothing in database, render empty tree. // return Ok(RepositoryView { // name: repository.name, // owner: request.repository.owner.clone(), // description: repository.description, // visibility: repository.visibility, // default_branch: repository.default_branch, // latest_commit: None, // tree_rev: None, // tree: vec![], // }); // } // } // Some(rev_name) => { // // Find the reference, otherwise return GitBackendError // match git // .find_reference(format!("refs/heads/{}", rev_name).as_str()) // .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string())) // { // Ok(reference) => reference.name().unwrap().to_string(), // Err(err) => return Err(Box::new(err).into()), // } // } // }; // // Get the git object as a commit // let rev = match git // .revparse_single(rev_name.as_str()) // .map_err(|_| GitBackendError::RefNotFound(rev_name.to_string())) // { // Ok(rev) => rev, // Err(err) => return Err(Box::new(err).into()), // }; // let commit = rev.as_commit().unwrap(); // // this is stupid // let mut current_path = rev_name.replace("refs/heads/", ""); // // Get the commit tree // let git_tree = if let Some(path) = &request.path { // // Add it to our full path string // current_path.push_str(format!("/{}", path).as_str()); // // Get the specified path, return an error if it wasn't found. // let entry = match commit // .tree() // .unwrap() // .get_path(&PathBuf::from(path)) // .map_err(|_| GitBackendError::PathNotFound(path.to_string())) // { // Ok(entry) => entry, // Err(err) => return Err(Box::new(err).into()), // }; // // Turn the entry into a git tree // entry.to_object(&git).unwrap().as_tree().unwrap().clone() // } else { // commit.tree().unwrap() // }; // // Iterate over the git tree and collect it into our own tree types // let mut tree = git_tree // .iter() // .map(|entry| { // let object_type = match entry.kind().unwrap() { // ObjectType::Tree => RepositoryObjectType::Tree, // ObjectType::Blob => RepositoryObjectType::Blob, // _ => unreachable!(), // }; // let mut tree_entry = // RepositoryTreeEntry::new(entry.name().unwrap(), object_type, entry.filemode()); // if request.extra_metadata { // // Get the file size if It's a blob // let object = entry.to_object(&git).unwrap(); // if let Some(blob) = object.as_blob() { // tree_entry.size = Some(blob.size()); // } // // Could possibly be done better // let path = if let Some(path) = current_path.split_once('/') { // format!("{}/{}", path.1, entry.name().unwrap()) // } else { // entry.name().unwrap().to_string() // }; // // Get the last commit made to the entry // if let Ok(last_commit) = // GitBackend::get_last_commit_of_file(&path, &git, commit) // { // tree_entry.last_commit = Some(last_commit); // } // } // tree_entry // }) // .collect::>(); // // Sort the tree alphabetically and with tree first // tree.sort_unstable_by_key(|entry| entry.name.to_lowercase()); // tree.sort_unstable_by_key(|entry| { // std::cmp::Reverse(format!("{:?}", entry.object_type).to_lowercase()) // }); // Ok(RepositoryView { // name: repository.name, // owner: request.repository.owner.clone(), // description: repository.description, // visibility: repository.visibility, // default_branch: repository.default_branch, // latest_commit: Some(Commit::from(commit.clone())), // tree_rev: Some(rev_name), // tree, // }) // } async fn repository_file_inspect( &mut self, _requester: Option<&User>, _request: &RepositoryFileInspectRequest, ) -> Result, Error> { todo!() } async fn repositories_for_user( &mut self, requester: Option<&User>, user: &User, ) -> Result, Error> { let mut repositories = sqlx::query_as!( GitRepository, r#"SELECT visibility as "visibility: _", owner_user, name, description, default_branch FROM repositories WHERE owner_user = $1"#, user.to_string() ) .fetch_many(&self.pg_pool); let mut result = vec![]; while let Some(Ok(Either::Right(repository))) = repositories.next().await { // Check if the requesting user is allowed to see the repository if !(matches!( repository.visibility, RepositoryVisibility::Unlisted | RepositoryVisibility::Private ) && Some(&repository.owner_user.clone()) != requester) { result.push(RepositorySummary { repository: Repository { owner: repository.owner_user.clone(), name: repository.name, instance: self.instance.clone(), }, owner: repository.owner_user.clone(), visibility: repository.visibility, description: repository.description, // TODO last_commit: None, }); } } Ok(result) } } impl IssuesBackend for GitBackend { fn issues_count( &mut self, _requester: Option<&User>, _request: &RepositoryIssuesCountRequest, ) -> Result { todo!() } fn issue_labels( &mut self, _requester: Option<&User>, _request: &RepositoryIssueLabelsRequest, ) -> Result, Error> { todo!() } fn issues( &mut self, _requester: Option<&User>, _request: &RepositoryIssuesRequest, ) -> Result, Error> { todo!() } } #[derive(Debug, sqlx::FromRow)] struct RepositoryMetadata { pub repository: String, pub name: String, pub value: String, }