use anyhow::Error; use async_trait::async_trait; use futures_util::StreamExt; use git2::ObjectType; use giterated_models::{ messages::repository::{ RepositoryCreateRequest, RepositoryCreateResponse, RepositoryFileInspectRequest, RepositoryFileInspectionResponse, RepositoryInfoRequest, RepositoryIssueLabelsRequest, RepositoryIssueLabelsResponse, RepositoryIssuesCountRequest, RepositoryIssuesCountResponse, RepositoryIssuesRequest, RepositoryIssuesResponse, }, model::{ instance::Instance, repository::{ Commit, Repository, RepositoryObjectType, RepositorySummary, RepositoryTreeEntry, RepositoryView, RepositoryVisibility, }, user::User, }, }; use sqlx::{Either, PgPool}; use std::path::{Path, PathBuf}; use thiserror::Error; use super::{IssuesBackend, 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, } impl GitBackend { pub fn new( pg_pool: &PgPool, repository_folder: &str, instance: impl ToOwned, ) -> Self { Self { pg_pool: pg_pool.clone(), repository_folder: repository_folder.to_string(), instance: instance.to_owned(), } } 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 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(RepositoryCreateResponse) } 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 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: None, tree_rev: Some(rev_name), tree, }) } async fn repository_file_inspect( &mut self, _requester: Option<&User>, _request: &RepositoryFileInspectRequest, ) -> Result { 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 { todo!() } fn issues( &mut self, _requester: Option<&User>, _request: &RepositoryIssuesRequest, ) -> Result { todo!() } }