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) } }