use anyhow::Error; use async_trait::async_trait; use git2::BranchType; use giterated_models::instance::{Instance, RepositoryCreateRequest}; use giterated_models::repository::{ AccessList, Commit, DefaultBranch, Description, IssueLabel, Repository, RepositoryBranch, RepositoryBranchesRequest, RepositoryChunkLine, RepositoryCommitBeforeRequest, RepositoryCommitFromIdRequest, RepositoryDiff, RepositoryDiffFile, RepositoryDiffFileChunk, RepositoryDiffFileInfo, RepositoryDiffFileStatus, RepositoryDiffPatchRequest, RepositoryDiffRequest, RepositoryFile, RepositoryFileFromIdRequest, RepositoryFileFromPathRequest, RepositoryFileInspectRequest, RepositoryIssue, RepositoryIssueLabelsRequest, RepositoryIssuesCountRequest, RepositoryIssuesRequest, RepositoryLastCommitOfFileRequest, RepositoryObjectType, RepositoryStatistics, RepositoryStatisticsRequest, RepositoryTreeEntry, RepositoryVisibility, Visibility, }; use giterated_models::user::User; use giterated_stack::{AuthenticatedUser, GiteratedStack}; 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 path in repository `{0}`")] PathNotFound(String), #[error("Couldn't find commit for path `{0}`")] LastCommitNotFound(String), #[error("Object ID `{0}` is invalid")] InvalidObjectId(String), #[error("Blob with ID `{0}` not found")] BlobNotFound(String), #[error("Tree with ID `{0}` not found")] TreeNotFound(String), #[error("Commit with ID `{0}` not found")] CommitNotFound(String), #[error("Parent for commit with ID `{0}` not found")] CommitParentNotFound(String), #[error("Failed diffing tree with ID `{0}` to tree with ID `{1}`")] FailedDiffing(String, String), } pub struct GitBackend { pub pg_pool: PgPool, pub repository_folder: String, pub instance: Instance, pub 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(), repository_folder: repository_folder.to_string(), instance, stack, } } pub async fn find_by_owner_user_name( &self, user: &User, repository_name: &str, ) -> Result { if let Ok(repository) = sqlx::query_as!(GitRepository, r#"SELECT owner_user, name, description, visibility as "visibility: _", default_branch FROM repositories WHERE owner_user = $1 AND name = $2"#, user.to_string(), repository_name) .fetch_one(&self.pg_pool.clone()) .await { Ok(repository) } else { Err(GitBackendError::RepositoryNotFound { owner_user: user.to_string(), name: repository_name.to_string(), }) } } pub async fn delete_by_owner_user_name( &self, user: &User, repository_name: &str, ) -> Result { if let Err(err) = std::fs::remove_dir_all(PathBuf::from(format!( "{}/{}/{}/{}", self.repository_folder, user.instance, user.username, repository_name ))) { let err = GitBackendError::CouldNotDeleteFromDisk(err); error!( "Couldn't delete repository from disk, this is bad! {:?}", err ); return Err(err); } // Delete the repository from the database match sqlx::query!( "DELETE FROM repositories WHERE owner_user = $1 AND name = $2", user.to_string(), repository_name ) .execute(&self.pg_pool.clone()) .await { Ok(deleted) => Ok(deleted.rows_affected()), Err(err) => Err(GitBackendError::FailedDeletingFromDatabase(err)), } } pub async fn open_repository_and_check_permissions( &self, owner: &User, name: &str, requester: &Option, ) -> 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()) } pub fn get_oid_from_reference( git: &git2::Repository, rev: Option<&str>, ) -> anyhow::Result { // Try and parse the input as a reference and get the object ID let mut tree_id = match rev { None => { if let Ok(head) = git.head() { // TODO: Fix for symbolic references head.target() } else { // Nothing in database, render empty tree. return Err(GitBackendError::HeadNotFound.into()); } } Some(rev_name) => { // Find the reference, otherwise return GitBackendError git.refname_to_id(rev_name).ok() } }; // If the reference wasn't found, try parsing it as a commit ID if tree_id.is_none() { if let Ok(oid) = git2::Oid::from_str(rev.as_ref().unwrap()) { tree_id = Some(oid) } } // If the commit ID wasn't found, try parsing it as a branch and otherwise return error if tree_id.is_none() { match git.find_branch(rev.as_ref().unwrap(), BranchType::Local) { Ok(branch) => tree_id = branch.get().target(), Err(_) => { return Err( Box::new(GitBackendError::RefNotFound(rev.unwrap().to_string())).into(), ) } } } // Should be safe? Ok(tree_id.unwrap()) } /// Gets the last commit in a rev pub fn get_last_commit_in_rev(git: &git2::Repository, rev: &str) -> anyhow::Result { let oid = Self::get_oid_from_reference(git, Some(rev))?; // 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] 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_by_owner_user_name(&request.owner, request.name.as_str()) .await?; // ??? Err(err) } } } async fn repository_file_inspect( &mut self, requester: &Option, repository: &Repository, request: &RepositoryFileInspectRequest, ) -> Result, Error> { let git = self .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) .await?; let tree_id = Self::get_oid_from_reference(&git, request.rev.as_deref())?; // unwrap might be dangerous? // Get the commit from the oid let commit = git.find_commit(tree_id).unwrap(); // 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: &Repository, request: &RepositoryFileFromIdRequest, ) -> Result { let git = self .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) .await?; // Parse the passed object id let oid = match git2::Oid::from_str(request.0.as_str()) { Ok(oid) => oid, Err(_) => { return Err(Box::new(GitBackendError::InvalidObjectId(request.0.clone())).into()) } }; // Find the file and turn it into our own struct let file = match git.find_blob(oid) { Ok(blob) => RepositoryFile { 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: &Repository, request: &RepositoryFileFromPathRequest, ) -> Result<(RepositoryFile, String), Error> { let git = self .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) .await?; let tree_id = Self::get_oid_from_reference(&git, request.rev.as_deref())?; // 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: &Repository, request: &RepositoryCommitFromIdRequest, ) -> Result { 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: &Repository, request: &RepositoryLastCommitOfFileRequest, ) -> Result { 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) } async fn repository_get_statistics( &mut self, requester: &Option, repository: &Repository, request: &RepositoryStatisticsRequest, ) -> Result { let git = self .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) .await?; let tree_id = Self::get_oid_from_reference(&git, request.rev.as_deref())?; // unwrap might be dangerous? // Get the commit from the oid let commit = git.find_commit(tree_id).unwrap(); // 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; } } } Ok(RepositoryStatistics { commits: GitBackend::get_total_commit_count(&git, &commit)?, branches, tags, }) } async fn repository_get_branches( &mut self, requester: &Option, repository: &Repository, _request: &RepositoryBranchesRequest, ) -> Result, Error> { let git = self .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) .await?; let mut branches = vec![]; for branch in git.branches(None)? { let branch = branch?; let Some(name) = branch.0.name().ok().flatten() else { continue; }; // TODO: Non UTF-8? let commit = GitBackend::get_last_commit_in_rev(&git, branch.0.get().name().unwrap()).ok(); // TODO: Implement stale with configurable age let stale = false; branches.push(RepositoryBranch { name: name.to_string(), stale, last_commit: commit, }) } Ok(branches) } async fn repository_diff( &mut self, requester: &Option, repository: &Repository, request: &RepositoryDiffRequest, ) -> Result { let git = self .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) .await?; // Parse the passed object ids let oid_old = 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: &Repository, request: &RepositoryDiffPatchRequest, ) -> Result { 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: &Repository, request: &RepositoryCommitBeforeRequest, ) -> Result { let git = self .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) .await?; // Parse the passed object id let oid = match git2::Oid::from_str(request.0.as_str()) { Ok(oid) => oid, Err(_) => { return Err(Box::new(GitBackendError::InvalidObjectId(request.0.clone())).into()) } }; // Find the commit using the parsed oid let commit = match git.find_commit(oid) { Ok(commit) => commit, Err(_) => return Err(Box::new(GitBackendError::CommitNotFound(oid.to_string())).into()), }; // Get the first parent it has let parent = 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, }