use anyhow::Error; use async_trait::async_trait; use futures_util::StreamExt; use git2::BranchType; use giterated_models::instance::{Instance, RepositoryCreateRequest}; use giterated_models::repository::{ Commit, DefaultBranch, Description, IssueLabel, LatestCommit, Repository, RepositoryCommitBeforeRequest, RepositoryDiff, RepositoryDiffRequest, RepositoryFile, RepositoryFileFromIdRequest, RepositoryFileInspectRequest, RepositoryIssue, RepositoryIssueLabelsRequest, RepositoryIssuesCountRequest, RepositoryIssuesRequest, RepositoryObjectType, RepositoryTreeEntry, RepositoryVisibility, Visibility, }; use giterated_models::settings::{AnySetting, Setting}; use giterated_models::user::{User, UserParseError}; use giterated_models::value::{AnyValue, GiteratedObjectValue}; use serde_json::Value; use sqlx::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), #[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), } 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)), } } pub async fn open_repository_and_check_permissions( &self, owner: &User, name: &str, requester: Option<&User>, ) -> 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(Some(requester)) { return Err(GitBackendError::RepositoryNotFound { owner_user: repository.owner_user.to_string(), name: repository.name.clone(), }); } } else if matches!(repository.visibility, RepositoryVisibility::Private) { info!("Unauthenticated"); // 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) => return Err(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.clone(), &repository.name) .await { Ok(true) } else { 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(unsafe { if name == Description::value_name() { AnyValue::from_raw(self.get_setting(repository, Description::name()).await?.0) } else if name == Visibility::value_name() { AnyValue::from_raw(self.get_setting(repository, Visibility::name()).await?.0) } else if name == DefaultBranch::value_name() { AnyValue::from_raw(self.get_setting(repository, DefaultBranch::name()).await?.0) } else if name == LatestCommit::value_name() { AnyValue::from_raw(serde_json::to_value(LatestCommit(None)).unwrap()) } else { 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>, repository: &Repository, request: &RepositoryFileInspectRequest, ) -> Result, Error> { let git = self .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) .await?; // Try and parse the input as a reference and get the object ID let mut tree_id = match &request.rev { None => { if let Ok(head) = git.head() { // TODO: Fix for symbolic references head.target() } else { // Nothing in database, render empty tree. return Ok(vec![]); } } Some(rev_name) => { // Find the reference, otherwise return GitBackendError git.refname_to_id(rev_name).ok() } }; // If the reference wasn't found, try parsing it as a commit ID if tree_id.is_none() { if let Ok(oid) = git2::Oid::from_str(request.rev.as_ref().unwrap()) { tree_id = Some(oid) } } // If the commit ID wasn't found, try parsing it as a branch and otherwise return error if tree_id.is_none() { match git.find_branch(request.rev.as_ref().unwrap(), BranchType::Local) { Ok(branch) => tree_id = branch.get().target(), Err(_) => { return Err(Box::new(GitBackendError::RefNotFound( request.rev.clone().unwrap(), )) .into()) } } } // unwrap might be dangerous? // Get the commit from the oid let commit = git.find_commit(tree_id.unwrap()).unwrap(); // this is stupid let mut current_path = request.rev.clone().unwrap_or_else(|| "master".to_string()); // 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() { git2::ObjectType::Tree => RepositoryObjectType::Tree, git2::ObjectType::Blob => RepositoryObjectType::Blob, _ => unreachable!(), }; let mut tree_entry = RepositoryTreeEntry::new( entry.id().to_string().as_str(), 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(tree) } async fn repository_file_from_id( &mut self, requester: Option<&User>, repository: &Repository, request: &RepositoryFileFromIdRequest, ) -> Result { let git = self .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) .await?; // Parse the passed object id let oid = match git2::Oid::from_str(request.0.as_str()) { Ok(oid) => oid, Err(_) => { return Err(Box::new(GitBackendError::InvalidObjectId(request.0.clone())).into()) } }; // Find the file and turn it into our own struct let file = match git.find_blob(oid) { Ok(blob) => RepositoryFile { content: blob.content().to_vec(), binary: blob.is_binary(), size: blob.size(), }, Err(_) => return Err(Box::new(GitBackendError::BlobNotFound(oid.to_string())).into()), }; Ok(file) } async fn repository_diff( &mut self, requester: Option<&User>, repository: &Repository, request: &RepositoryDiffRequest, ) -> Result { let git = self .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) .await?; // Parse the passed object ids let oid_old = match git2::Oid::from_str(request.old_id.as_str()) { Ok(oid) => oid, Err(_) => { return Err( Box::new(GitBackendError::InvalidObjectId(request.old_id.clone())).into(), ) } }; let oid_new = match git2::Oid::from_str(request.new_id.as_str()) { Ok(oid) => oid, Err(_) => { return Err( Box::new(GitBackendError::InvalidObjectId(request.new_id.clone())).into(), ) } }; // Get the trees of those object ids let tree_old = match git.find_tree(oid_old) { Ok(tree) => tree, Err(_) => { return Err(Box::new(GitBackendError::TreeNotFound(oid_old.to_string())).into()) } }; let tree_new = match git.find_tree(oid_new) { Ok(tree) => tree, Err(_) => { return Err(Box::new(GitBackendError::TreeNotFound(oid_new.to_string())).into()) } }; // Diff the two trees against each other let diff = match git.diff_tree_to_tree(Some(&tree_old), Some(&tree_new), None) { Ok(diff) => diff, Err(_) => { return Err(Box::new(GitBackendError::FailedDiffing( oid_old.to_string(), oid_new.to_string(), )) .into()) } }; // Should be safe to unwrap? let stats = diff.stats().unwrap(); // Honestly not quite sure what is going on here, could not find documentation. // Print the entire patch let mut patch = String::new(); diff.print(git2::DiffFormat::Patch, |_, _, line| { patch.push_str(std::str::from_utf8(line.content()).unwrap()); true }) .unwrap(); Ok(RepositoryDiff { files_changed: stats.files_changed(), insertions: stats.insertions(), deletions: stats.deletions(), patch, }) } async fn repository_commit_before( &mut self, requester: Option<&User>, repository: &Repository, request: &RepositoryCommitBeforeRequest, ) -> Result { let git = self .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) .await?; // Parse the passed object id let oid = match git2::Oid::from_str(request.0.as_str()) { Ok(oid) => oid, Err(_) => { return Err(Box::new(GitBackendError::InvalidObjectId(request.0.clone())).into()) } }; // Find the commit using the parsed oid let commit = match git.find_commit(oid) { Ok(commit) => commit, Err(_) => return Err(Box::new(GitBackendError::CommitNotFound(oid.to_string())).into()), }; // Get the first parent it has let parent = match commit.parent(0) { Ok(parent) => Commit::from(parent), Err(_) => { return Err(Box::new(GitBackendError::CommitParentNotFound(oid.to_string())).into()) } }; Ok(parent) } } 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, }