diff --git a/giterated-daemon/src/backend/git.rs b/giterated-daemon/src/backend/git.rs deleted file mode 100644 index 7cb0491..0000000 --- a/giterated-daemon/src/backend/git.rs +++ /dev/null @@ -1,1402 +0,0 @@ -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, BranchStaleAfter, Commit, CommitSignature, DefaultBranch, Description, IssueLabel, - Repository, RepositoryBranch, RepositoryBranchFilter, RepositoryBranchRequest, - RepositoryBranchesRequest, RepositoryChunkLine, RepositoryCommitBeforeRequest, - RepositoryCommitFromIdRequest, RepositoryDiff, RepositoryDiffFile, RepositoryDiffFileChunk, - RepositoryDiffFileInfo, RepositoryDiffFileStatus, RepositoryDiffPatchRequest, - RepositoryDiffRequest, RepositoryFile, RepositoryFileFromIdRequest, - RepositoryFileFromPathRequest, RepositoryFileInspectRequest, RepositoryIssue, - RepositoryIssueLabelsRequest, RepositoryIssuesCountRequest, RepositoryIssuesRequest, - RepositoryLastCommitOfFileRequest, RepositoryObjectType, RepositoryStatistics, - RepositoryStatisticsRequest, RepositoryTag, 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::{Path, PathBuf}, - sync::Arc, -}; -use thiserror::Error; -use tokio::sync::OnceCell; - -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 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 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), -} - -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), - } - } - - // 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 { - trace!("Getting last commit for file: {}", path); - - 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()))? - } - - /// Gets the total amount of commits using revwalk - pub fn get_total_commit_count( - git: &git2::Repository, - start_commit: &git2::Commit, - ) -> anyhow::Result { - // TODO: There must be a better way - let mut revwalk = git.revwalk()?; - revwalk.set_sorting(git2::Sort::TIME)?; - revwalk.push(start_commit.id())?; - - Ok(revwalk.count()) - } - - /// 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())) - } - } - - /// Gets the last commit in a rev - pub fn get_last_commit_in_rev( - git: &git2::Repository, - rev: &str, - default_branch: &DefaultBranch, - ) -> anyhow::Result { - let oid = Self::get_oid_from_reference(git, Some(rev), default_branch)?; - - // Walk through the repository commit graph starting at our rev - let mut revwalk = git.revwalk()?; - revwalk.set_sorting(git2::Sort::TIME)?; - revwalk.push(oid)?; - - if let Some(Ok(commit_oid)) = revwalk.next() { - if let Ok(commit) = git - .find_commit(commit_oid) - .map_err(|_| GitBackendError::CommitNotFound(commit_oid.to_string())) - { - return Ok(Commit::from(commit)); - } - } - - Err(GitBackendError::RefNotFound(oid.to_string()).into()) - } -} - -#[async_trait(?Send)] -impl RepositoryBackend for GitBackend { - 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) - } - } - - 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> { - 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?; - // Try and find the tree_id/branch - let tree_id = - match Self::get_oid_from_reference(&git, request.rev.as_deref(), &default_branch) { - Ok(oid) => oid, - Err(GitBackendError::HeadNotFound) => return Ok(vec![]), - Err(err) => return Err(err.into()), - }; - - // Get the commit from the oid - let commit = match git.find_commit(tree_id) { - Ok(commit) => commit, - // If the commit isn't found, it's generally safe to assume the tree is empty. - Err(_) => return Ok(vec![]), - }; - - // this is stupid - let rev = request.rev.clone().unwrap_or_else(|| "master".to_string()); - let mut current_path = rev.clone(); - - // 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()); - } - - // Get the path to the folder the file is in by removing the rev from current_path - let mut path = current_path.replace(&rev, ""); - if path.starts_with('/') { - path.remove(0); - } - - // Format it as the path + file name - let full_path = if path.is_empty() { - entry.name().unwrap().to_string() - } else { - format!("{}/{}", path, entry.name().unwrap()) - }; - - // Get the last commit made to the entry - if let Ok(last_commit) = - GitBackend::get_last_commit_of_file(&full_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, - repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, - OperationState(_operation_state): OperationState, - request: &RepositoryFileFromIdRequest, - ) -> Result { - let repository = repository_object.object(); - 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 { - id: blob.id().to_string(), - 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_file_from_path( - &mut self, - requester: &Option, - repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, - OperationState(operation_state): OperationState, - request: &RepositoryFileFromPathRequest, - ) -> Result<(RepositoryFile, String), Error> { - 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 = Self::get_oid_from_reference(&git, request.rev.as_deref(), &default_branch)?; - - // unwrap might be dangerous? - // Get the commit from the oid - let commit = git.find_commit(tree_id).unwrap(); - - // this is stupid - let mut current_path = request.rev.clone().unwrap_or_else(|| "master".to_string()); - - // Add it to our full path string - current_path.push_str(format!("/{}", request.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(request.path.clone())) - .map_err(|_| GitBackendError::PathNotFound(request.path.to_string())) - { - Ok(entry) => entry, - Err(err) => return Err(Box::new(err).into()), - }; - - // Find the file and turn it into our own struct - let file = match git.find_blob(entry.id()) { - Ok(blob) => RepositoryFile { - id: blob.id().to_string(), - content: blob.content().to_vec(), - binary: blob.is_binary(), - size: blob.size(), - }, - Err(_) => { - return Err(Box::new(GitBackendError::BlobNotFound(entry.id().to_string())).into()) - } - }; - - Ok((file, commit.id().to_string())) - } - - async fn repository_commit_from_id( - &mut self, - requester: &Option, - repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, - OperationState(_operation_state): OperationState, - request: &RepositoryCommitFromIdRequest, - ) -> Result { - let repository = repository_object.object(); - let git = self - .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) - .await?; - - // Parse the passed object ids - let oid = git2::Oid::from_str(request.0.as_str()) - .map_err(|_| GitBackendError::InvalidObjectId(request.0.clone()))?; - - // Get the commit from the oid - let commit = git - .find_commit(oid) - .map_err(|_| GitBackendError::CommitNotFound(oid.to_string()))?; - - Ok(Commit::from(commit)) - } - - 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 { - let repository = repository_object.object(); - let git = self - .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) - .await?; - - // Parse the passed object ids - let oid = git2::Oid::from_str(&request.start_commit) - .map_err(|_| GitBackendError::InvalidObjectId(request.start_commit.clone()))?; - - // Get the commit from the oid - let commit = git - .find_commit(oid) - .map_err(|_| GitBackendError::CommitNotFound(oid.to_string()))?; - - // Find the last commit of the file - let commit = GitBackend::get_last_commit_of_file(request.path.as_str(), &git, &commit)?; - - Ok(commit) - } - - /// 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> { - let repository = repository_object.object(); - let git = self - .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) - .await?; - - let default_branch_name = repository_object - .get::(&operation_state) - .await?; - let default_branch = git - .find_branch(&default_branch_name.0, BranchType::Local) - .map_err(|_| GitBackendError::DefaultNotFound)?; - - // Get the stale(after X seconds) setting - let stale_after = repository_object - .get::(&operation_state) - .await - .unwrap_or_default() - .0; - - // Could be done better with the RepositoryBranchFilter::None check done beforehand. - let mut filtered_branches = git - .branches(None)? - .filter_map(|branch| { - let branch = branch.ok()?.0; - - let Some(name) = branch.name().ok().flatten() else { - return None; - }; - - // TODO: Non UTF-8? - let Some(commit) = GitBackend::get_last_commit_in_rev( - &git, - branch.get().name().unwrap(), - &default_branch_name, - ) - .ok() else { - return None; - }; - - let stale = chrono::Utc::now() - .naive_utc() - .signed_duration_since(commit.time) - .num_seconds() - > stale_after.into(); - - // Filter based on if the branch is stale or not - if request.filter != RepositoryBranchFilter::None { - #[allow(clippy::if_same_then_else)] - if stale && request.filter == RepositoryBranchFilter::Active { - return None; - } else if !stale && request.filter == RepositoryBranchFilter::Stale { - return None; - } - } - - Some((name.to_string(), branch, stale, commit)) - }) - .collect::>(); - - // Get the total amount of filtered branches - let branch_count = filtered_branches.len(); - - if let Some(search) = &request.search { - // TODO: Caching - // Search by sorting using a simple fuzzy search algorithm - filtered_branches.sort_by(|(n1, _, _, _), (n2, _, _, _)| { - strsim::damerau_levenshtein(search, n1) - .cmp(&strsim::damerau_levenshtein(search, n2)) - }); - } else { - // Sort the branches by commit date - filtered_branches.sort_by(|(_, _, _, c1), (_, _, _, c2)| c2.time.cmp(&c1.time)); - } - - // Go to the requested position - let mut filtered_branches = filtered_branches.iter().skip(request.range.0); - - let mut branches = vec![]; - - // Iterate through the filtered branches using the passed range - for _ in request.range.0..request.range.1 { - let Some((name, branch, stale, commit)) = filtered_branches.next() else { - break; - }; - - // Get how many commits are ahead of and behind of the head - let ahead_behind_default = - if default_branch.get().target().is_some() && branch.get().target().is_some() { - git.graph_ahead_behind( - branch.get().target().unwrap(), - default_branch.get().target().unwrap(), - ) - .ok() - } else { - None - }; - - branches.push(RepositoryBranch { - name: name.to_string(), - stale: *stale, - last_commit: Some(commit.clone()), - ahead_behind_default, - }) - } - - Ok((branches, branch_count)) - } - - async fn repository_get_branch( - &mut self, - requester: &Option, - repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, - OperationState(operation_state): OperationState, - request: &RepositoryBranchRequest, - ) -> Result { - let repository = repository_object.object(); - let git = self - .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) - .await?; - - // TODO: Don't duplicate search when the default branch and the requested one are the same - // Get the default branch to compare against - let default_branch_name = repository_object - .get::(&operation_state) - .await?; - let default_branch = git - .find_branch(&default_branch_name.0, BranchType::Local) - .map_err(|_| GitBackendError::DefaultNotFound)?; - - // Find the requested branch - let branch = git - .find_branch(&request.name, BranchType::Local) - .map_err(|_| GitBackendError::BranchNotFound(request.name.clone()))?; - - // Get the stale(after X seconds) setting - let stale_after = repository_object - .get::(&operation_state) - .await - .unwrap_or_default() - .0; - - // TODO: Non UTF-8? - let last_commit = GitBackend::get_last_commit_in_rev( - &git, - branch.get().name().unwrap(), - &default_branch_name, - ) - .ok(); - - let stale = if let Some(ref last_commit) = last_commit { - chrono::Utc::now() - .naive_utc() - .signed_duration_since(last_commit.time) - .num_seconds() - > stale_after.into() - } else { - // TODO: Make sure it's acceptable to return false here - false - }; - - // Get how many commits are ahead of and behind of the head - let ahead_behind_default = - if default_branch.get().target().is_some() && branch.get().target().is_some() { - git.graph_ahead_behind( - branch.get().target().unwrap(), - default_branch.get().target().unwrap(), - ) - .ok() - } else { - None - }; - - Ok(RepositoryBranch { - name: request.name.clone(), - stale, - last_commit, - ahead_behind_default, - }) - } - - /// .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> { - let repository = repository_object.object(); - let git = self - .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) - .await?; - - let mut tags = vec![]; - - // Iterate over each tag - let _ = git.tag_foreach(|id, name| { - // Get the name in utf8 - let name = String::from_utf8_lossy(name).replacen("refs/tags/", "", 1); - - // Find the tag so we can get the messages attached if any - if let Ok(tag) = git.find_tag(id) { - // Get the tag message and split it into a summary and body - let (summary, body) = if let Some(message) = tag.message() { - // Iterate over the lines - let mut lines = message - .lines() - .map(|line| { - // 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); - }); - - whitespace_removed - }) - .collect::>(); - - let summary = Some(lines.remove(0)); - let body = if lines.is_empty() { - None - } else { - Some(lines.join("\n")) - }; - - (summary, body) - } else { - (None, None) - }; - - // Get the commit the tag is (possibly) pointing to - let commit = tag - .peel() - .map(|obj| obj.into_commit().ok()) - .ok() - .flatten() - .map(|c| Commit::from(c)); - // Get the author of the tag - let author: Option = tag.tagger().map(|s| s.into()); - // Get the time the tag or pointed commit was created - let time = if let Some(ref author) = author { - Some(author.time) - } else { - // Get possible commit time if the tag has no author time - commit.as_ref().map(|c| c.time.clone()) - }; - - tags.push(RepositoryTag { - id: id.to_string(), - name: name.to_string(), - summary, - body, - author, - time, - commit, - }); - } else { - // Lightweight commit, we try and find the commit it's pointing to - let commit = git.find_commit(id).ok().map(|c| Commit::from(c)); - - tags.push(RepositoryTag { - id: id.to_string(), - name: name.to_string(), - summary: None, - body: None, - author: None, - time: commit.as_ref().map(|c| c.time.clone()), - commit, - }); - }; - - true - }); - - // Get the total amount of tags - let tag_count = tags.len(); - - if let Some(search) = &request.search { - // TODO: Caching - // Search by sorting using a simple fuzzy search algorithm - tags.sort_by(|n1, n2| { - strsim::damerau_levenshtein(search, &n1.name) - .cmp(&strsim::damerau_levenshtein(search, &n2.name)) - }); - } else { - // Sort the tags using their creation or pointer date - tags.sort_by(|t1, t2| t2.time.cmp(&t1.time)); - } - - // Get the requested range of tags - let tags = tags - .into_iter() - .skip(request.range.0) - .take(request.range.1.saturating_sub(request.range.0)) - .collect::>(); - - Ok((tags, tag_count)) - } - - async fn repository_diff( - &mut self, - requester: &Option, - repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, - OperationState(_operation_state): OperationState, - request: &RepositoryDiffRequest, - ) -> Result { - let repository = repository_object.object(); - let git = self - .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) - .await?; - - // Parse the passed object ids - let oid_old = git2::Oid::from_str(request.old_id.as_str()) - .map_err(|_| GitBackendError::InvalidObjectId(request.old_id.clone()))?; - let oid_new = git2::Oid::from_str(request.new_id.as_str()) - .map_err(|_| GitBackendError::InvalidObjectId(request.new_id.clone()))?; - - // Get the ids associates commits - let commit_old = git - .find_commit(oid_old) - .map_err(|_| GitBackendError::CommitNotFound(oid_old.to_string()))?; - let commit_new = git - .find_commit(oid_new) - .map_err(|_| GitBackendError::CommitNotFound(oid_new.to_string()))?; - - // Get the commit trees - let tree_old = commit_old - .tree() - .map_err(|_| GitBackendError::TreeNotFound(oid_old.to_string()))?; - let tree_new = commit_new - .tree() - .map_err(|_| GitBackendError::TreeNotFound(oid_new.to_string()))?; - - // Diff the two trees against each other - let diff = git - .diff_tree_to_tree(Some(&tree_old), Some(&tree_new), None) - .map_err(|_| { - GitBackendError::FailedDiffing(oid_old.to_string(), oid_new.to_string()) - })?; - - // Should be safe to unwrap? - let stats = diff.stats().unwrap(); - let mut files: Vec = vec![]; - - diff.deltas().enumerate().for_each(|(i, delta)| { - // Parse the old file info from the delta - let old_file_info = match delta.old_file().exists() { - true => Some(RepositoryDiffFileInfo { - id: delta.old_file().id().to_string(), - path: delta - .old_file() - .path() - .unwrap() - .to_str() - .unwrap() - .to_string(), - size: delta.old_file().size(), - binary: delta.old_file().is_binary(), - }), - false => None, - }; - // Parse the new file info from the delta - let new_file_info = match delta.new_file().exists() { - true => Some(RepositoryDiffFileInfo { - id: delta.new_file().id().to_string(), - path: delta - .new_file() - .path() - .unwrap() - .to_str() - .unwrap() - .to_string(), - size: delta.new_file().size(), - binary: delta.new_file().is_binary(), - }), - false => None, - }; - - let mut chunks: Vec = vec![]; - if let Some(patch) = git2::Patch::from_diff(&diff, i).ok().flatten() { - for chunk_num in 0..patch.num_hunks() { - if let Ok((chunk, chunk_num_lines)) = patch.hunk(chunk_num) { - let mut lines: Vec = vec![]; - - for line_num in 0..chunk_num_lines { - if let Ok(line) = patch.line_in_hunk(chunk_num, line_num) { - if let Ok(line_utf8) = String::from_utf8(line.content().to_vec()) { - lines.push(RepositoryChunkLine { - change_type: line.origin_value().into(), - content: line_utf8, - old_line_num: line.old_lineno(), - new_line_num: line.new_lineno(), - }); - } - - continue; - } - } - - chunks.push(RepositoryDiffFileChunk { - header: String::from_utf8(chunk.header().to_vec()).ok(), - old_start: chunk.old_start(), - old_lines: chunk.old_lines(), - new_start: chunk.new_start(), - new_lines: chunk.new_lines(), - lines, - }); - } - } - }; - - let file = RepositoryDiffFile { - status: RepositoryDiffFileStatus::from(delta.status()), - old_file_info, - new_file_info, - chunks, - }; - - files.push(file); - }); - - Ok(RepositoryDiff { - new_commit: Commit::from(commit_new), - files_changed: stats.files_changed(), - insertions: stats.insertions(), - deletions: stats.deletions(), - files, - }) - } - - async fn repository_diff_patch( - &mut self, - requester: &Option, - repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, - OperationState(_operation_state): OperationState, - request: &RepositoryDiffPatchRequest, - ) -> Result { - let repository = repository_object.object(); - let git = self - .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) - .await?; - - // Parse the passed object ids - let oid_old = git2::Oid::from_str(request.old_id.as_str()) - .map_err(|_| GitBackendError::InvalidObjectId(request.old_id.clone()))?; - let oid_new = git2::Oid::from_str(request.new_id.as_str()) - .map_err(|_| GitBackendError::InvalidObjectId(request.new_id.clone()))?; - - // Get the ids associates commits - let commit_old = git - .find_commit(oid_old) - .map_err(|_| GitBackendError::CommitNotFound(oid_old.to_string()))?; - let commit_new = git - .find_commit(oid_new) - .map_err(|_| GitBackendError::CommitNotFound(oid_new.to_string()))?; - - // Get the commit trees - let tree_old = commit_old - .tree() - .map_err(|_| GitBackendError::TreeNotFound(oid_old.to_string()))?; - let tree_new = commit_new - .tree() - .map_err(|_| GitBackendError::TreeNotFound(oid_new.to_string()))?; - - // Diff the two trees against each other - let diff = git - .diff_tree_to_tree(Some(&tree_old), Some(&tree_new), None) - .map_err(|_| { - GitBackendError::FailedDiffing(oid_old.to_string(), oid_new.to_string()) - })?; - - // Print the entire patch - let mut patch = String::new(); - - diff.print(git2::DiffFormat::Patch, |_, _, line| { - match line.origin() { - '+' | '-' | ' ' => patch.push(line.origin()), - _ => {} - } - patch.push_str(std::str::from_utf8(line.content()).unwrap()); - true - }) - .unwrap(); - - Ok(patch) - } - - async fn repository_commit_before( - &mut self, - requester: &Option, - repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, - OperationState(_operation_state): OperationState, - request: &RepositoryCommitBeforeRequest, - ) -> Result { - let repository = repository_object.object(); - 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 = commit.parent(0); - if let Ok(parent) = parent { - return Ok(Commit::from(parent)); - } else { - // TODO: See if can be done better - // Walk through the repository commit graph starting at our current commit - let mut revwalk = git.revwalk()?; - revwalk.set_sorting(git2::Sort::TIME)?; - revwalk.push(commit.id())?; - - if let Some(Ok(before_commit_oid)) = revwalk.next() { - // Find the commit using the parsed oid - if let Ok(before_commit) = git.find_commit(before_commit_oid) { - return Ok(Commit::from(before_commit)); - } - } - - Err(Box::new(GitBackendError::CommitParentNotFound(oid.to_string())).into()) - } - } -} - -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, -} diff --git a/giterated-daemon/src/backend/git/branches.rs b/giterated-daemon/src/backend/git/branches.rs new file mode 100644 index 0000000..799dd8e --- /dev/null +++ b/giterated-daemon/src/backend/git/branches.rs @@ -0,0 +1,203 @@ +use anyhow::Error; +use git2::BranchType; +use giterated_models::{ + object::Object, + repository::{ + BranchStaleAfter, DefaultBranch, Repository, RepositoryBranch, RepositoryBranchFilter, + RepositoryBranchRequest, RepositoryBranchesRequest, + }, +}; +use giterated_stack::{AuthenticatedUser, GiteratedStack, OperationState, StackOperationState}; + +use super::{GitBackend, GitBackendError}; + +impl GitBackend { + /// .0: List of branches filtering by passed requirements. + /// .1: Total amount of branches after being filtered + pub async fn handle_repository_get_branches( + &mut self, + requester: &Option, + repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, + OperationState(operation_state): OperationState, + request: &RepositoryBranchesRequest, + ) -> Result<(Vec, usize), Error> { + let repository = repository_object.object(); + let git = self + .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) + .await?; + + let default_branch_name = repository_object + .get::(&operation_state) + .await?; + let default_branch = git + .find_branch(&default_branch_name.0, BranchType::Local) + .map_err(|_| GitBackendError::DefaultNotFound)?; + + // Get the stale(after X seconds) setting + let stale_after = repository_object + .get::(&operation_state) + .await + .unwrap_or_default() + .0; + + // Could be done better with the RepositoryBranchFilter::None check done beforehand. + let mut filtered_branches = git + .branches(None)? + .filter_map(|branch| { + let branch = branch.ok()?.0; + + let Some(name) = branch.name().ok().flatten() else { + return None; + }; + + // TODO: Non UTF-8? + let Some(commit) = GitBackend::get_last_commit_in_rev( + &git, + branch.get().name().unwrap(), + &default_branch_name, + ) + .ok() else { + return None; + }; + + let stale = chrono::Utc::now() + .naive_utc() + .signed_duration_since(commit.time) + .num_seconds() + > stale_after.into(); + + // Filter based on if the branch is stale or not + if request.filter != RepositoryBranchFilter::None { + #[allow(clippy::if_same_then_else)] + if stale && request.filter == RepositoryBranchFilter::Active { + return None; + } else if !stale && request.filter == RepositoryBranchFilter::Stale { + return None; + } + } + + Some((name.to_string(), branch, stale, commit)) + }) + .collect::>(); + + // Get the total amount of filtered branches + let branch_count = filtered_branches.len(); + + if let Some(search) = &request.search { + // TODO: Caching + // Search by sorting using a simple fuzzy search algorithm + filtered_branches.sort_by(|(n1, _, _, _), (n2, _, _, _)| { + strsim::damerau_levenshtein(search, n1) + .cmp(&strsim::damerau_levenshtein(search, n2)) + }); + } else { + // Sort the branches by commit date + filtered_branches.sort_by(|(_, _, _, c1), (_, _, _, c2)| c2.time.cmp(&c1.time)); + } + + // Go to the requested position + let mut filtered_branches = filtered_branches.iter().skip(request.range.0); + + let mut branches = vec![]; + + // Iterate through the filtered branches using the passed range + for _ in request.range.0..request.range.1 { + let Some((name, branch, stale, commit)) = filtered_branches.next() else { + break; + }; + + // Get how many commits are ahead of and behind of the head + let ahead_behind_default = + if default_branch.get().target().is_some() && branch.get().target().is_some() { + git.graph_ahead_behind( + branch.get().target().unwrap(), + default_branch.get().target().unwrap(), + ) + .ok() + } else { + None + }; + + branches.push(RepositoryBranch { + name: name.to_string(), + stale: *stale, + last_commit: Some(commit.clone()), + ahead_behind_default, + }) + } + + Ok((branches, branch_count)) + } + + pub async fn handle_repository_get_branch( + &mut self, + requester: &Option, + repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, + OperationState(operation_state): OperationState, + request: &RepositoryBranchRequest, + ) -> Result { + let repository = repository_object.object(); + let git = self + .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) + .await?; + + // TODO: Don't duplicate search when the default branch and the requested one are the same + // Get the default branch to compare against + let default_branch_name = repository_object + .get::(&operation_state) + .await?; + let default_branch = git + .find_branch(&default_branch_name.0, BranchType::Local) + .map_err(|_| GitBackendError::DefaultNotFound)?; + + // Find the requested branch + let branch = git + .find_branch(&request.name, BranchType::Local) + .map_err(|_| GitBackendError::BranchNotFound(request.name.clone()))?; + + // Get the stale(after X seconds) setting + let stale_after = repository_object + .get::(&operation_state) + .await + .unwrap_or_default() + .0; + + // TODO: Non UTF-8? + let last_commit = GitBackend::get_last_commit_in_rev( + &git, + branch.get().name().unwrap(), + &default_branch_name, + ) + .ok(); + + let stale = if let Some(ref last_commit) = last_commit { + chrono::Utc::now() + .naive_utc() + .signed_duration_since(last_commit.time) + .num_seconds() + > stale_after.into() + } else { + // TODO: Make sure it's acceptable to return false here + false + }; + + // Get how many commits are ahead of and behind of the head + let ahead_behind_default = + if default_branch.get().target().is_some() && branch.get().target().is_some() { + git.graph_ahead_behind( + branch.get().target().unwrap(), + default_branch.get().target().unwrap(), + ) + .ok() + } else { + None + }; + + Ok(RepositoryBranch { + name: request.name.clone(), + stale, + last_commit, + ahead_behind_default, + }) + } +} diff --git a/giterated-daemon/src/backend/git/commit.rs b/giterated-daemon/src/backend/git/commit.rs new file mode 100644 index 0000000..b66e8e7 --- /dev/null +++ b/giterated-daemon/src/backend/git/commit.rs @@ -0,0 +1,123 @@ +use anyhow::Error; +use giterated_models::{ + object::Object, + repository::{ + Commit, DefaultBranch, Repository, RepositoryCommitBeforeRequest, + RepositoryCommitFromIdRequest, + }, +}; +use giterated_stack::{AuthenticatedUser, GiteratedStack, OperationState, StackOperationState}; + +use super::{GitBackend, GitBackendError}; + +impl GitBackend { + /// Gets the total amount of commits using revwalk + pub fn get_total_commit_count( + git: &git2::Repository, + start_commit: &git2::Commit, + ) -> anyhow::Result { + // TODO: There must be a better way + let mut revwalk = git.revwalk()?; + revwalk.set_sorting(git2::Sort::TIME)?; + revwalk.push(start_commit.id())?; + + Ok(revwalk.count()) + } + + /// Gets the last commit in a rev + pub fn get_last_commit_in_rev( + git: &git2::Repository, + rev: &str, + default_branch: &DefaultBranch, + ) -> anyhow::Result { + let oid = Self::get_oid_from_reference(git, Some(rev), default_branch)?; + + // Walk through the repository commit graph starting at our rev + let mut revwalk = git.revwalk()?; + revwalk.set_sorting(git2::Sort::TIME)?; + revwalk.push(oid)?; + + if let Some(Ok(commit_oid)) = revwalk.next() { + if let Ok(commit) = git + .find_commit(commit_oid) + .map_err(|_| GitBackendError::CommitNotFound(commit_oid.to_string())) + { + return Ok(Commit::from(commit)); + } + } + + Err(GitBackendError::RefNotFound(oid.to_string()).into()) + } + + pub async fn handle_repository_commit_from_id( + &mut self, + requester: &Option, + repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, + OperationState(_operation_state): OperationState, + request: &RepositoryCommitFromIdRequest, + ) -> Result { + let repository = repository_object.object(); + let git = self + .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) + .await?; + + // Parse the passed object ids + let oid = git2::Oid::from_str(request.0.as_str()) + .map_err(|_| GitBackendError::InvalidObjectId(request.0.clone()))?; + + // Get the commit from the oid + let commit = git + .find_commit(oid) + .map_err(|_| GitBackendError::CommitNotFound(oid.to_string()))?; + + Ok(Commit::from(commit)) + } + + pub async fn handle_repository_commit_before( + &mut self, + requester: &Option, + repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, + OperationState(_operation_state): OperationState, + request: &RepositoryCommitBeforeRequest, + ) -> Result { + let repository = repository_object.object(); + 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 = commit.parent(0); + if let Ok(parent) = parent { + return Ok(Commit::from(parent)); + } else { + // TODO: See if can be done better + // Walk through the repository commit graph starting at our current commit + let mut revwalk = git.revwalk()?; + revwalk.set_sorting(git2::Sort::TIME)?; + revwalk.push(commit.id())?; + + if let Some(Ok(before_commit_oid)) = revwalk.next() { + // Find the commit using the parsed oid + if let Ok(before_commit) = git.find_commit(before_commit_oid) { + return Ok(Commit::from(before_commit)); + } + } + + Err(Box::new(GitBackendError::CommitParentNotFound(oid.to_string())).into()) + } + } +} diff --git a/giterated-daemon/src/backend/git/diff.rs b/giterated-daemon/src/backend/git/diff.rs new file mode 100644 index 0000000..fcbe427 --- /dev/null +++ b/giterated-daemon/src/backend/git/diff.rs @@ -0,0 +1,202 @@ +use anyhow::Error; +use giterated_models::{ + object::Object, + repository::{ + Commit, Repository, RepositoryChunkLine, RepositoryDiff, RepositoryDiffFile, + RepositoryDiffFileChunk, RepositoryDiffFileInfo, RepositoryDiffFileStatus, + RepositoryDiffPatchRequest, RepositoryDiffRequest, + }, +}; +use giterated_stack::{AuthenticatedUser, GiteratedStack, OperationState, StackOperationState}; + +use super::{GitBackend, GitBackendError}; + +impl GitBackend { + pub async fn handle_repository_diff( + &mut self, + requester: &Option, + repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, + OperationState(_operation_state): OperationState, + request: &RepositoryDiffRequest, + ) -> Result { + let repository = repository_object.object(); + let git = self + .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) + .await?; + + // Parse the passed object ids + let oid_old = git2::Oid::from_str(request.old_id.as_str()) + .map_err(|_| GitBackendError::InvalidObjectId(request.old_id.clone()))?; + let oid_new = git2::Oid::from_str(request.new_id.as_str()) + .map_err(|_| GitBackendError::InvalidObjectId(request.new_id.clone()))?; + + // Get the ids associates commits + let commit_old = git + .find_commit(oid_old) + .map_err(|_| GitBackendError::CommitNotFound(oid_old.to_string()))?; + let commit_new = git + .find_commit(oid_new) + .map_err(|_| GitBackendError::CommitNotFound(oid_new.to_string()))?; + + // Get the commit trees + let tree_old = commit_old + .tree() + .map_err(|_| GitBackendError::TreeNotFound(oid_old.to_string()))?; + let tree_new = commit_new + .tree() + .map_err(|_| GitBackendError::TreeNotFound(oid_new.to_string()))?; + + // Diff the two trees against each other + let diff = git + .diff_tree_to_tree(Some(&tree_old), Some(&tree_new), None) + .map_err(|_| { + GitBackendError::FailedDiffing(oid_old.to_string(), oid_new.to_string()) + })?; + + // Should be safe to unwrap? + let stats = diff.stats().unwrap(); + let mut files: Vec = vec![]; + + diff.deltas().enumerate().for_each(|(i, delta)| { + // Parse the old file info from the delta + let old_file_info = match delta.old_file().exists() { + true => Some(RepositoryDiffFileInfo { + id: delta.old_file().id().to_string(), + path: delta + .old_file() + .path() + .unwrap() + .to_str() + .unwrap() + .to_string(), + size: delta.old_file().size(), + binary: delta.old_file().is_binary(), + }), + false => None, + }; + // Parse the new file info from the delta + let new_file_info = match delta.new_file().exists() { + true => Some(RepositoryDiffFileInfo { + id: delta.new_file().id().to_string(), + path: delta + .new_file() + .path() + .unwrap() + .to_str() + .unwrap() + .to_string(), + size: delta.new_file().size(), + binary: delta.new_file().is_binary(), + }), + false => None, + }; + + let mut chunks: Vec = vec![]; + if let Some(patch) = git2::Patch::from_diff(&diff, i).ok().flatten() { + for chunk_num in 0..patch.num_hunks() { + if let Ok((chunk, chunk_num_lines)) = patch.hunk(chunk_num) { + let mut lines: Vec = vec![]; + + for line_num in 0..chunk_num_lines { + if let Ok(line) = patch.line_in_hunk(chunk_num, line_num) { + if let Ok(line_utf8) = String::from_utf8(line.content().to_vec()) { + lines.push(RepositoryChunkLine { + change_type: line.origin_value().into(), + content: line_utf8, + old_line_num: line.old_lineno(), + new_line_num: line.new_lineno(), + }); + } + + continue; + } + } + + chunks.push(RepositoryDiffFileChunk { + header: String::from_utf8(chunk.header().to_vec()).ok(), + old_start: chunk.old_start(), + old_lines: chunk.old_lines(), + new_start: chunk.new_start(), + new_lines: chunk.new_lines(), + lines, + }); + } + } + }; + + let file = RepositoryDiffFile { + status: RepositoryDiffFileStatus::from(delta.status()), + old_file_info, + new_file_info, + chunks, + }; + + files.push(file); + }); + + Ok(RepositoryDiff { + new_commit: Commit::from(commit_new), + files_changed: stats.files_changed(), + insertions: stats.insertions(), + deletions: stats.deletions(), + files, + }) + } + + pub async fn handle_repository_diff_patch( + &mut self, + requester: &Option, + repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, + OperationState(_operation_state): OperationState, + request: &RepositoryDiffPatchRequest, + ) -> Result { + let repository = repository_object.object(); + let git = self + .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) + .await?; + + // Parse the passed object ids + let oid_old = git2::Oid::from_str(request.old_id.as_str()) + .map_err(|_| GitBackendError::InvalidObjectId(request.old_id.clone()))?; + let oid_new = git2::Oid::from_str(request.new_id.as_str()) + .map_err(|_| GitBackendError::InvalidObjectId(request.new_id.clone()))?; + + // Get the ids associates commits + let commit_old = git + .find_commit(oid_old) + .map_err(|_| GitBackendError::CommitNotFound(oid_old.to_string()))?; + let commit_new = git + .find_commit(oid_new) + .map_err(|_| GitBackendError::CommitNotFound(oid_new.to_string()))?; + + // Get the commit trees + let tree_old = commit_old + .tree() + .map_err(|_| GitBackendError::TreeNotFound(oid_old.to_string()))?; + let tree_new = commit_new + .tree() + .map_err(|_| GitBackendError::TreeNotFound(oid_new.to_string()))?; + + // Diff the two trees against each other + let diff = git + .diff_tree_to_tree(Some(&tree_old), Some(&tree_new), None) + .map_err(|_| { + GitBackendError::FailedDiffing(oid_old.to_string(), oid_new.to_string()) + })?; + + // Print the entire patch + let mut patch = String::new(); + + diff.print(git2::DiffFormat::Patch, |_, _, line| { + match line.origin() { + '+' | '-' | ' ' => patch.push(line.origin()), + _ => {} + } + patch.push_str(std::str::from_utf8(line.content()).unwrap()); + true + }) + .unwrap(); + + Ok(patch) + } +} diff --git a/giterated-daemon/src/backend/git/file.rs b/giterated-daemon/src/backend/git/file.rs new file mode 100644 index 0000000..499a689 --- /dev/null +++ b/giterated-daemon/src/backend/git/file.rs @@ -0,0 +1,288 @@ +use std::path::{Path, PathBuf}; + +use anyhow::Error; +use giterated_models::{ + object::Object, + repository::{ + Commit, DefaultBranch, Repository, RepositoryFile, RepositoryFileFromIdRequest, + RepositoryFileFromPathRequest, RepositoryFileInspectRequest, + RepositoryLastCommitOfFileRequest, RepositoryObjectType, RepositoryTreeEntry, + }, +}; +use giterated_stack::{AuthenticatedUser, GiteratedStack, OperationState, StackOperationState}; + +use crate::backend::git::GitBackendError; + +use super::GitBackend; + +impl GitBackend { + // TODO: Cache this and general repository tree and invalidate select files on push + // TODO: Find better and faster technique for this + /// Gets the last commit made to a file by revwalking starting at the passed commit and sorting by time + pub fn get_last_commit_of_file( + path: &str, + git: &git2::Repository, + start_commit: &git2::Commit, + ) -> anyhow::Result { + trace!("Getting last commit for file: {}", path); + + 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()))? + } + + /// If the OID can't be found because there's no repository head, this will return an empty `Vec`. + pub async fn handle_repository_file_inspect( + &mut self, + requester: &Option, + repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, + OperationState(operation_state): OperationState, + request: &RepositoryFileInspectRequest, + ) -> Result, Error> { + 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?; + // Try and find the tree_id/branch + let tree_id = + match Self::get_oid_from_reference(&git, request.rev.as_deref(), &default_branch) { + Ok(oid) => oid, + Err(GitBackendError::HeadNotFound) => return Ok(vec![]), + Err(err) => return Err(err.into()), + }; + + // Get the commit from the oid + let commit = match git.find_commit(tree_id) { + Ok(commit) => commit, + // If the commit isn't found, it's generally safe to assume the tree is empty. + Err(_) => return Ok(vec![]), + }; + + // this is stupid + let rev = request.rev.clone().unwrap_or_else(|| "master".to_string()); + let mut current_path = rev.clone(); + + // 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()); + } + + // Get the path to the folder the file is in by removing the rev from current_path + let mut path = current_path.replace(&rev, ""); + if path.starts_with('/') { + path.remove(0); + } + + // Format it as the path + file name + let full_path = if path.is_empty() { + entry.name().unwrap().to_string() + } else { + format!("{}/{}", path, entry.name().unwrap()) + }; + + // Get the last commit made to the entry + if let Ok(last_commit) = + GitBackend::get_last_commit_of_file(&full_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) + } + + pub async fn handle_repository_file_from_id( + &mut self, + requester: &Option, + repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, + OperationState(_operation_state): OperationState, + request: &RepositoryFileFromIdRequest, + ) -> Result { + let repository = repository_object.object(); + 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 { + id: blob.id().to_string(), + 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) + } + + pub async fn handle_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> { + 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 = Self::get_oid_from_reference(&git, request.rev.as_deref(), &default_branch)?; + + // unwrap might be dangerous? + // Get the commit from the oid + let commit = git.find_commit(tree_id).unwrap(); + + // this is stupid + let mut current_path = request.rev.clone().unwrap_or_else(|| "master".to_string()); + + // Add it to our full path string + current_path.push_str(format!("/{}", request.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(request.path.clone())) + .map_err(|_| GitBackendError::PathNotFound(request.path.to_string())) + { + Ok(entry) => entry, + Err(err) => return Err(Box::new(err).into()), + }; + + // Find the file and turn it into our own struct + let file = match git.find_blob(entry.id()) { + Ok(blob) => RepositoryFile { + id: blob.id().to_string(), + content: blob.content().to_vec(), + binary: blob.is_binary(), + size: blob.size(), + }, + Err(_) => { + return Err(Box::new(GitBackendError::BlobNotFound(entry.id().to_string())).into()) + } + }; + + Ok((file, commit.id().to_string())) + } + + pub async fn handle_repository_last_commit_of_file( + &mut self, + requester: &Option, + repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, + OperationState(_operation_state): OperationState, + request: &RepositoryLastCommitOfFileRequest, + ) -> Result { + let repository = repository_object.object(); + let git = self + .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) + .await?; + + // Parse the passed object ids + let oid = git2::Oid::from_str(&request.start_commit) + .map_err(|_| GitBackendError::InvalidObjectId(request.start_commit.clone()))?; + + // Get the commit from the oid + let commit = git + .find_commit(oid) + .map_err(|_| GitBackendError::CommitNotFound(oid.to_string()))?; + + // Find the last commit of the file + let commit = GitBackend::get_last_commit_of_file(request.path.as_str(), &git, &commit)?; + + Ok(commit) + } +} diff --git a/giterated-daemon/src/backend/git/mod.rs b/giterated-daemon/src/backend/git/mod.rs new file mode 100644 index 0000000..88331aa --- /dev/null +++ b/giterated-daemon/src/backend/git/mod.rs @@ -0,0 +1,701 @@ +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, 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 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), +} + +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.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 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, +} diff --git a/giterated-daemon/src/backend/git/tags.rs b/giterated-daemon/src/backend/git/tags.rs new file mode 100644 index 0000000..723a512 --- /dev/null +++ b/giterated-daemon/src/backend/git/tags.rs @@ -0,0 +1,135 @@ +use anyhow::Error; +use giterated_models::{ + object::Object, + repository::{Commit, CommitSignature, Repository, RepositoryTag, RepositoryTagsRequest}, +}; +use giterated_stack::{AuthenticatedUser, GiteratedStack, OperationState, StackOperationState}; + +use super::GitBackend; + +impl GitBackend { + /// .0: List of tags in passed range + /// .1: Total amount of tags + pub async fn handle_repository_get_tags( + &mut self, + requester: &Option, + repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, + OperationState(_operation_state): OperationState, + request: &RepositoryTagsRequest, + ) -> Result<(Vec, usize), Error> { + let repository = repository_object.object(); + let git = self + .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) + .await?; + + let mut tags = vec![]; + + // Iterate over each tag + let _ = git.tag_foreach(|id, name| { + // Get the name in utf8 + let name = String::from_utf8_lossy(name).replacen("refs/tags/", "", 1); + + // Find the tag so we can get the messages attached if any + if let Ok(tag) = git.find_tag(id) { + // Get the tag message and split it into a summary and body + let (summary, body) = if let Some(message) = tag.message() { + // Iterate over the lines + let mut lines = message + .lines() + .map(|line| { + // 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); + }); + + whitespace_removed + }) + .collect::>(); + + let summary = Some(lines.remove(0)); + let body = if lines.is_empty() { + None + } else { + Some(lines.join("\n")) + }; + + (summary, body) + } else { + (None, None) + }; + + // Get the commit the tag is (possibly) pointing to + let commit = tag + .peel() + .map(|obj| obj.into_commit().ok()) + .ok() + .flatten() + .map(|c| Commit::from(c)); + // Get the author of the tag + let author: Option = tag.tagger().map(|s| s.into()); + // Get the time the tag or pointed commit was created + let time = if let Some(ref author) = author { + Some(author.time) + } else { + // Get possible commit time if the tag has no author time + commit.as_ref().map(|c| c.time.clone()) + }; + + tags.push(RepositoryTag { + id: id.to_string(), + name: name.to_string(), + summary, + body, + author, + time, + commit, + }); + } else { + // Lightweight commit, we try and find the commit it's pointing to + let commit = git.find_commit(id).ok().map(|c| Commit::from(c)); + + tags.push(RepositoryTag { + id: id.to_string(), + name: name.to_string(), + summary: None, + body: None, + author: None, + time: commit.as_ref().map(|c| c.time.clone()), + commit, + }); + }; + + true + }); + + // Get the total amount of tags + let tag_count = tags.len(); + + if let Some(search) = &request.search { + // TODO: Caching + // Search by sorting using a simple fuzzy search algorithm + tags.sort_by(|n1, n2| { + strsim::damerau_levenshtein(search, &n1.name) + .cmp(&strsim::damerau_levenshtein(search, &n2.name)) + }); + } else { + // Sort the tags using their creation or pointer date + tags.sort_by(|t1, t2| t2.time.cmp(&t1.time)); + } + + // Get the requested range of tags + let tags = tags + .into_iter() + .skip(request.range.0) + .take(request.range.1.saturating_sub(request.range.0)) + .collect::>(); + + Ok((tags, tag_count)) + } +}