diff --git a/giterated-daemon/src/backend/git.rs b/giterated-daemon/src/backend/git.rs index c113f3e..58da38d 100644 --- a/giterated-daemon/src/backend/git.rs +++ b/giterated-daemon/src/backend/git.rs @@ -9,7 +9,7 @@ use giterated_models::repository::{ RepositoryCommitBeforeRequest, RepositoryDiff, RepositoryDiffRequest, RepositoryFile, RepositoryFileFromIdRequest, RepositoryFileInspectRequest, RepositoryIssue, RepositoryIssueLabelsRequest, RepositoryIssuesCountRequest, RepositoryIssuesRequest, - RepositoryObjectType, RepositoryTreeEntry, RepositoryVisibility, Visibility, + RepositoryObjectType, RepositoryTreeEntry, RepositoryVisibility, Visibility, RepositoryDiffFile, RepositoryDiffFileInfo, RepositoryDiffFileStatus, RepositoryDiffFileChunk, RepositoryDiffPatchRequest, }; use giterated_models::settings::{AnySetting, Setting}; use giterated_models::user::{User, UserParseError}; @@ -740,8 +740,116 @@ impl RepositoryBackend for GitBackend { // 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(line_utf8); + } + + break; + } + + lines.push(String::new()); + } + + chunks.push(RepositoryDiffFileChunk { + 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<&User>, + 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()) + })?; - // Honestly not quite sure what is going on here, could not find documentation. // Print the entire patch let mut patch = String::new(); @@ -755,13 +863,7 @@ impl RepositoryBackend for GitBackend { }) .unwrap(); - Ok(RepositoryDiff { - new_commit: Commit::from(commit_new), - files_changed: stats.files_changed(), - insertions: stats.insertions(), - deletions: stats.deletions(), - patch, - }) + Ok(patch) } async fn repository_commit_before( diff --git a/giterated-daemon/src/backend/mod.rs b/giterated-daemon/src/backend/mod.rs index aeb956b..fba1812 100644 --- a/giterated-daemon/src/backend/mod.rs +++ b/giterated-daemon/src/backend/mod.rs @@ -19,7 +19,7 @@ use giterated_models::repository::{ Commit, IssueLabel, Repository, RepositoryCommitBeforeRequest, RepositoryDiff, RepositoryDiffRequest, RepositoryFile, RepositoryFileFromIdRequest, RepositoryFileInspectRequest, RepositoryIssue, RepositoryIssueLabelsRequest, - RepositoryIssuesCountRequest, RepositoryIssuesRequest, RepositorySummary, RepositoryTreeEntry, + RepositoryIssuesCountRequest, RepositoryIssuesRequest, RepositorySummary, RepositoryTreeEntry, RepositoryDiffPatchRequest, }; use giterated_models::settings::AnySetting; use giterated_models::user::User; @@ -50,6 +50,12 @@ pub trait RepositoryBackend { repository: &Repository, request: &RepositoryDiffRequest, ) -> Result; + async fn repository_diff_patch( + &mut self, + requester: Option<&User>, + repository: &Repository, + request: &RepositoryDiffPatchRequest, + ) -> Result; async fn repository_commit_before( &mut self, requester: Option<&User>, diff --git a/giterated-daemon/src/database_backend/handler.rs b/giterated-daemon/src/database_backend/handler.rs index 0b2e180..cd6db73 100644 --- a/giterated-daemon/src/database_backend/handler.rs +++ b/giterated-daemon/src/database_backend/handler.rs @@ -8,7 +8,7 @@ use giterated_models::{ Commit, DefaultBranch, Description, LatestCommit, Repository, RepositoryCommitBeforeRequest, RepositoryDiff, RepositoryDiffRequest, RepositoryFile, RepositoryFileFromIdRequest, RepositoryFileInspectRequest, RepositoryInfoRequest, - RepositorySummary, RepositoryView, Visibility, + RepositorySummary, RepositoryView, Visibility, RepositoryDiffPatchRequest, }, settings::{AnySetting, GetSetting, GetSettingError, SetSetting, SetSettingError}, user::{User, UserRepositoriesRequest}, @@ -194,13 +194,40 @@ pub fn repository_diff( .unwrap(); let mut repository_backend = state.repository_backend.lock().await; - let file = repository_backend + let diff = repository_backend .repository_diff(None, object.object(), &operation) .await .map_err(|err| OperationError::Internal(format!("{:?}", err)))?; drop(repository_backend); - Ok(file) + Ok(diff) + } + .boxed() +} + +pub fn repository_diff_patch( + object: &Repository, + operation: RepositoryDiffPatchRequest, + state: DatabaseBackend, + operation_state: StackOperationState, + backend: BackendWrapper, +) -> BoxFuture<'static, Result>> { + let object = object.clone(); + + async move { + let object = backend + .get_object::(&object.to_string(), &operation_state) + .await + .unwrap(); + + let mut repository_backend = state.repository_backend.lock().await; + let patch = repository_backend + .repository_diff_patch(None, object.object(), &operation) + .await + .map_err(|err| OperationError::Internal(format!("{:?}", err)))?; + drop(repository_backend); + + Ok(patch) } .boxed() } diff --git a/giterated-daemon/src/database_backend/mod.rs b/giterated-daemon/src/database_backend/mod.rs index d32f1e3..68c1c8b 100644 --- a/giterated-daemon/src/database_backend/mod.rs +++ b/giterated-daemon/src/database_backend/mod.rs @@ -19,7 +19,7 @@ use crate::backend::{RepositoryBackend, UserBackend}; use self::handler::{ repository_commit_before, repository_diff, repository_file_from_id, repository_get_setting, repository_get_value, repository_info, repository_set_setting, user_get_repositories, - user_get_setting, user_get_value, user_set_setting, + user_get_setting, user_get_value, user_set_setting, repository_diff_patch, }; #[derive(Clone, Debug)] @@ -79,6 +79,7 @@ impl DatabaseBackend { .insert(repository_info) .insert(repository_file_from_id) .insert(repository_diff) + .insert(repository_diff_patch) .insert(repository_commit_before) .insert(repository_get_value) .insert(repository_get_setting) @@ -117,7 +118,7 @@ mod test { use giterated_models::repository::{ Commit, Description, Repository, RepositoryCommitBeforeRequest, RepositoryDiff, RepositoryDiffRequest, RepositoryFile, RepositoryFileFromIdRequest, - RepositoryFileInspectRequest, RepositorySummary, RepositoryTreeEntry, + RepositoryFileInspectRequest, RepositorySummary, RepositoryTreeEntry, RepositoryDiffPatchRequest, }; use giterated_models::settings::AnySetting; use giterated_models::user::{DisplayName, User}; @@ -221,6 +222,14 @@ mod test { ) -> Result { todo!() } + async fn repository_diff_patch( + &mut self, + _requester: Option<&User>, + _repository: &Repository, + _request: &RepositoryDiffPatchRequest, + ) -> Result { + todo!() + } async fn repository_commit_before( &mut self, _requester: Option<&User>, diff --git a/giterated-models/src/repository/mod.rs b/giterated-models/src/repository/mod.rs index 57fd47c..a07d87a 100644 --- a/giterated-models/src/repository/mod.rs +++ b/giterated-models/src/repository/mod.rs @@ -157,8 +157,89 @@ pub struct RepositoryDiff { pub insertions: usize, /// Total number of deletions pub deletions: usize, - /// Preferably unified patch, probably a git patch. - pub patch: String, + /// List of changed files + pub files: Vec, +} + +/// Represents the type of change made to a [`RepositoryDiffFile`] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum RepositoryDiffFileStatus { + /// No changes + Unmodified, + /// Content changed between old and new + Modified, + /// Renamed between old and new + Renamed, + /// Copied from another old entry + Copied, + /// Ignored item in workdir + Ignored, + /// Untracked item in workdir + Untracked, + /// Type of file changed between old and new + Typechange, + /// File is unreadable + Unreadable, + /// File in the index is conflicted + Conflicted, +} + +impl From for RepositoryDiffFileStatus { + fn from(status: git2::Delta) -> Self { + match status { + git2::Delta::Unmodified => Self::Unmodified, + git2::Delta::Modified => Self::Modified, + git2::Delta::Renamed => Self::Renamed, + git2::Delta::Copied => Self::Copied, + git2::Delta::Ignored => Self::Ignored, + git2::Delta::Untracked => Self::Untracked, + git2::Delta::Typechange => Self::Typechange, + git2::Delta::Unreadable => Self::Unreadable, + git2::Delta::Conflicted => Self::Conflicted, + _ => unreachable!(), + } + } +} + +/// Represents a single file of a diff +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RepositoryDiffFile { + /// The type of change made to this file + pub status: RepositoryDiffFileStatus, + /// "From" side of the diff, can be nonexistent if file for example got added for the first time + pub old_file_info: Option, + /// "To" side of the diff, can be nonexistent if file got removed + pub new_file_info: Option, + /// Individual chunks of changes in this file + pub chunks: Vec, +} + +/// Represents one side of a file diff +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RepositoryDiffFileInfo { + /// ID of the file + pub id: String, + /// Path of the entry relative to the working directory of the repository + pub path: String, + /// Size in bytes + pub size: u64, + /// If the file is binary or not + pub binary: bool, +} + +/// Represents a single chunk of a file diff +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RepositoryDiffFileChunk { + /// Starting line number of the old file + pub old_start: u32, + /// Number of lines in "from" side of this chunk + pub old_lines: u32, + /// Starting line number of the new file + pub new_start: u32, + /// Number of lines in "to" side of this chunk + pub new_lines: u32, + /// Lines of the chunk + pub lines: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/giterated-models/src/repository/operations.rs b/giterated-models/src/repository/operations.rs index 326d2fa..f3fdf9f 100644 --- a/giterated-models/src/repository/operations.rs +++ b/giterated-models/src/repository/operations.rs @@ -70,6 +70,26 @@ impl GiteratedOperation for RepositoryDiffRequest { type Failure = RepositoryError; } +/// A request to get the difference between two repository trees as a unified git patch. +/// +/// # Authentication +/// - Instance Authentication +/// - Validate request against the `issued_for` public key +/// - Validate User token against the user's instance's public key +/// # Authorization +/// - User Authorization +/// - Potential User permissions checks +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RepositoryDiffPatchRequest { + pub old_id: String, + pub new_id: String, +} + +impl GiteratedOperation for RepositoryDiffPatchRequest { + type Success = String; + type Failure = RepositoryError; +} + /// A request to get the commit before the one with the passed id /// /// # Authentication @@ -197,6 +217,19 @@ impl + std::fmt::Debug> Object<'_, S .await } + pub async fn diff_patch( + &mut self, + old_id: String, + new_id: String, + operation_state: &S, + ) -> Result> { + self.request::( + RepositoryDiffPatchRequest { old_id, new_id }, + operation_state, + ) + .await + } + pub async fn commit_before( &mut self, id: String,