Experimental repository diff structure
parent: tbd commit: 0ef533d
Showing 6 changed files with 275 insertions and 17 deletions
giterated-daemon/src/backend/git.rs
@@ -9,7 +9,7 @@ use giterated_models::repository::{ | ||
9 | 9 | RepositoryCommitBeforeRequest, RepositoryDiff, RepositoryDiffRequest, RepositoryFile, |
10 | 10 | RepositoryFileFromIdRequest, RepositoryFileInspectRequest, RepositoryIssue, |
11 | 11 | RepositoryIssueLabelsRequest, RepositoryIssuesCountRequest, RepositoryIssuesRequest, |
12 | RepositoryObjectType, RepositoryTreeEntry, RepositoryVisibility, Visibility, | |
12 | RepositoryObjectType, RepositoryTreeEntry, RepositoryVisibility, Visibility, RepositoryDiffFile, RepositoryDiffFileInfo, RepositoryDiffFileStatus, RepositoryDiffFileChunk, RepositoryDiffPatchRequest, | |
13 | 13 | }; |
14 | 14 | use giterated_models::settings::{AnySetting, Setting}; |
15 | 15 | use giterated_models::user::{User, UserParseError}; |
@@ -740,8 +740,116 @@ impl RepositoryBackend for GitBackend { | ||
740 | 740 | |
741 | 741 | // Should be safe to unwrap? |
742 | 742 | let stats = diff.stats().unwrap(); |
743 | let mut files: Vec<RepositoryDiffFile> = vec![]; | |
744 | ||
745 | diff.deltas().enumerate().for_each(|(i, delta)| { | |
746 | // Parse the old file info from the delta | |
747 | let old_file_info = match delta.old_file().exists() { | |
748 | true => Some(RepositoryDiffFileInfo { | |
749 | id: delta.old_file().id().to_string(), | |
750 | path: delta.old_file().path().unwrap().to_str().unwrap().to_string(), | |
751 | size: delta.old_file().size(), | |
752 | binary: delta.old_file().is_binary() | |
753 | }), | |
754 | false => None, | |
755 | }; | |
756 | // Parse the new file info from the delta | |
757 | let new_file_info = match delta.new_file().exists() { | |
758 | true => Some(RepositoryDiffFileInfo { | |
759 | id: delta.new_file().id().to_string(), | |
760 | path: delta.new_file().path().unwrap().to_str().unwrap().to_string(), | |
761 | size: delta.new_file().size(), | |
762 | binary: delta.new_file().is_binary() | |
763 | }), | |
764 | false => None, | |
765 | }; | |
766 | ||
767 | let mut chunks: Vec<RepositoryDiffFileChunk> = vec![]; | |
768 | if let Some(patch) = git2::Patch::from_diff(&diff, i).ok().flatten() { | |
769 | for chunk_num in 0..patch.num_hunks() { | |
770 | if let Ok((chunk, chunk_num_lines)) = patch.hunk(chunk_num) { | |
771 | let mut lines: Vec<String> = vec![]; | |
772 | for line_num in 0..chunk_num_lines { | |
773 | if let Ok(line) = patch.line_in_hunk(chunk_num, line_num) { | |
774 | if let Ok(line_utf8) = String::from_utf8(line.content().to_vec()) { | |
775 | lines.push(line_utf8); | |
776 | } | |
777 | ||
778 | break; | |
779 | } | |
780 | ||
781 | lines.push(String::new()); | |
782 | } | |
783 | ||
784 | chunks.push(RepositoryDiffFileChunk { | |
785 | old_start: chunk.old_start(), | |
786 | old_lines: chunk.old_lines(), | |
787 | new_start: chunk.new_start(), | |
788 | new_lines: chunk.new_lines(), | |
789 | lines, | |
790 | }); | |
791 | } | |
792 | } | |
793 | }; | |
794 | ||
795 | let file = RepositoryDiffFile { | |
796 | status: RepositoryDiffFileStatus::from(delta.status()), | |
797 | old_file_info, | |
798 | new_file_info, | |
799 | chunks, | |
800 | }; | |
801 | ||
802 | files.push(file); | |
803 | }); | |
804 | ||
805 | Ok(RepositoryDiff { | |
806 | new_commit: Commit::from(commit_new), | |
807 | files_changed: stats.files_changed(), | |
808 | insertions: stats.insertions(), | |
809 | deletions: stats.deletions(), | |
810 | files, | |
811 | }) | |
812 | } | |
813 | ||
814 | async fn repository_diff_patch( | |
815 | &mut self, | |
816 | requester: Option<&User>, | |
817 | repository: &Repository, | |
818 | request: &RepositoryDiffPatchRequest, | |
819 | ) -> Result<String, Error> { | |
820 | let git = self | |
821 | .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) | |
822 | .await?; | |
823 | ||
824 | // Parse the passed object ids | |
825 | let oid_old = git2::Oid::from_str(request.old_id.as_str()) | |
826 | .map_err(|_| GitBackendError::InvalidObjectId(request.old_id.clone()))?; | |
827 | let oid_new = git2::Oid::from_str(request.new_id.as_str()) | |
828 | .map_err(|_| GitBackendError::InvalidObjectId(request.new_id.clone()))?; | |
829 | ||
830 | // Get the ids associates commits | |
831 | let commit_old = git | |
832 | .find_commit(oid_old) | |
833 | .map_err(|_| GitBackendError::CommitNotFound(oid_old.to_string()))?; | |
834 | let commit_new = git | |
835 | .find_commit(oid_new) | |
836 | .map_err(|_| GitBackendError::CommitNotFound(oid_new.to_string()))?; | |
837 | ||
838 | // Get the commit trees | |
839 | let tree_old = commit_old | |
840 | .tree() | |
841 | .map_err(|_| GitBackendError::TreeNotFound(oid_old.to_string()))?; | |
842 | let tree_new = commit_new | |
843 | .tree() | |
844 | .map_err(|_| GitBackendError::TreeNotFound(oid_new.to_string()))?; | |
845 | ||
846 | // Diff the two trees against each other | |
847 | let diff = git | |
848 | .diff_tree_to_tree(Some(&tree_old), Some(&tree_new), None) | |
849 | .map_err(|_| { | |
850 | GitBackendError::FailedDiffing(oid_old.to_string(), oid_new.to_string()) | |
851 | })?; | |
743 | 852 | |
744 | // Honestly not quite sure what is going on here, could not find documentation. | |
745 | 853 | // Print the entire patch |
746 | 854 | let mut patch = String::new(); |
747 | 855 | |
@@ -755,13 +863,7 @@ impl RepositoryBackend for GitBackend { | ||
755 | 863 | }) |
756 | 864 | .unwrap(); |
757 | 865 | |
758 | Ok(RepositoryDiff { | |
759 | new_commit: Commit::from(commit_new), | |
760 | files_changed: stats.files_changed(), | |
761 | insertions: stats.insertions(), | |
762 | deletions: stats.deletions(), | |
763 | patch, | |
764 | }) | |
866 | Ok(patch) | |
765 | 867 | } |
766 | 868 | |
767 | 869 | async fn repository_commit_before( |
giterated-daemon/src/backend/mod.rs
@@ -19,7 +19,7 @@ use giterated_models::repository::{ | ||
19 | 19 | Commit, IssueLabel, Repository, RepositoryCommitBeforeRequest, RepositoryDiff, |
20 | 20 | RepositoryDiffRequest, RepositoryFile, RepositoryFileFromIdRequest, |
21 | 21 | RepositoryFileInspectRequest, RepositoryIssue, RepositoryIssueLabelsRequest, |
22 | RepositoryIssuesCountRequest, RepositoryIssuesRequest, RepositorySummary, RepositoryTreeEntry, | |
22 | RepositoryIssuesCountRequest, RepositoryIssuesRequest, RepositorySummary, RepositoryTreeEntry, RepositoryDiffPatchRequest, | |
23 | 23 | }; |
24 | 24 | use giterated_models::settings::AnySetting; |
25 | 25 | use giterated_models::user::User; |
@@ -50,6 +50,12 @@ pub trait RepositoryBackend { | ||
50 | 50 | repository: &Repository, |
51 | 51 | request: &RepositoryDiffRequest, |
52 | 52 | ) -> Result<RepositoryDiff, Error>; |
53 | async fn repository_diff_patch( | |
54 | &mut self, | |
55 | requester: Option<&User>, | |
56 | repository: &Repository, | |
57 | request: &RepositoryDiffPatchRequest, | |
58 | ) -> Result<String, Error>; | |
53 | 59 | async fn repository_commit_before( |
54 | 60 | &mut self, |
55 | 61 | requester: Option<&User>, |
giterated-daemon/src/database_backend/handler.rs
@@ -8,7 +8,7 @@ use giterated_models::{ | ||
8 | 8 | Commit, DefaultBranch, Description, LatestCommit, Repository, |
9 | 9 | RepositoryCommitBeforeRequest, RepositoryDiff, RepositoryDiffRequest, RepositoryFile, |
10 | 10 | RepositoryFileFromIdRequest, RepositoryFileInspectRequest, RepositoryInfoRequest, |
11 | RepositorySummary, RepositoryView, Visibility, | |
11 | RepositorySummary, RepositoryView, Visibility, RepositoryDiffPatchRequest, | |
12 | 12 | }, |
13 | 13 | settings::{AnySetting, GetSetting, GetSettingError, SetSetting, SetSettingError}, |
14 | 14 | user::{User, UserRepositoriesRequest}, |
@@ -194,13 +194,40 @@ pub fn repository_diff( | ||
194 | 194 | .unwrap(); |
195 | 195 | |
196 | 196 | let mut repository_backend = state.repository_backend.lock().await; |
197 | let file = repository_backend | |
197 | let diff = repository_backend | |
198 | 198 | .repository_diff(None, object.object(), &operation) |
199 | 199 | .await |
200 | 200 | .map_err(|err| OperationError::Internal(format!("{:?}", err)))?; |
201 | 201 | drop(repository_backend); |
202 | 202 | |
203 | Ok(file) | |
203 | Ok(diff) | |
204 | } | |
205 | .boxed() | |
206 | } | |
207 | ||
208 | pub fn repository_diff_patch( | |
209 | object: &Repository, | |
210 | operation: RepositoryDiffPatchRequest, | |
211 | state: DatabaseBackend, | |
212 | operation_state: StackOperationState, | |
213 | backend: BackendWrapper, | |
214 | ) -> BoxFuture<'static, Result<String, OperationError<RepositoryError>>> { | |
215 | let object = object.clone(); | |
216 | ||
217 | async move { | |
218 | let object = backend | |
219 | .get_object::<Repository>(&object.to_string(), &operation_state) | |
220 | .await | |
221 | .unwrap(); | |
222 | ||
223 | let mut repository_backend = state.repository_backend.lock().await; | |
224 | let patch = repository_backend | |
225 | .repository_diff_patch(None, object.object(), &operation) | |
226 | .await | |
227 | .map_err(|err| OperationError::Internal(format!("{:?}", err)))?; | |
228 | drop(repository_backend); | |
229 | ||
230 | Ok(patch) | |
204 | 231 | } |
205 | 232 | .boxed() |
206 | 233 | } |
giterated-daemon/src/database_backend/mod.rs
@@ -19,7 +19,7 @@ use crate::backend::{RepositoryBackend, UserBackend}; | ||
19 | 19 | use self::handler::{ |
20 | 20 | repository_commit_before, repository_diff, repository_file_from_id, repository_get_setting, |
21 | 21 | repository_get_value, repository_info, repository_set_setting, user_get_repositories, |
22 | user_get_setting, user_get_value, user_set_setting, | |
22 | user_get_setting, user_get_value, user_set_setting, repository_diff_patch, | |
23 | 23 | }; |
24 | 24 | |
25 | 25 | #[derive(Clone, Debug)] |
@@ -79,6 +79,7 @@ impl DatabaseBackend { | ||
79 | 79 | .insert(repository_info) |
80 | 80 | .insert(repository_file_from_id) |
81 | 81 | .insert(repository_diff) |
82 | .insert(repository_diff_patch) | |
82 | 83 | .insert(repository_commit_before) |
83 | 84 | .insert(repository_get_value) |
84 | 85 | .insert(repository_get_setting) |
@@ -117,7 +118,7 @@ mod test { | ||
117 | 118 | use giterated_models::repository::{ |
118 | 119 | Commit, Description, Repository, RepositoryCommitBeforeRequest, RepositoryDiff, |
119 | 120 | RepositoryDiffRequest, RepositoryFile, RepositoryFileFromIdRequest, |
120 | RepositoryFileInspectRequest, RepositorySummary, RepositoryTreeEntry, | |
121 | RepositoryFileInspectRequest, RepositorySummary, RepositoryTreeEntry, RepositoryDiffPatchRequest, | |
121 | 122 | }; |
122 | 123 | use giterated_models::settings::AnySetting; |
123 | 124 | use giterated_models::user::{DisplayName, User}; |
@@ -221,6 +222,14 @@ mod test { | ||
221 | 222 | ) -> Result<RepositoryDiff, Error> { |
222 | 223 | todo!() |
223 | 224 | } |
225 | async fn repository_diff_patch( | |
226 | &mut self, | |
227 | _requester: Option<&User>, | |
228 | _repository: &Repository, | |
229 | _request: &RepositoryDiffPatchRequest, | |
230 | ) -> Result<String, Error> { | |
231 | todo!() | |
232 | } | |
224 | 233 | async fn repository_commit_before( |
225 | 234 | &mut self, |
226 | 235 | _requester: Option<&User>, |
giterated-models/src/repository/mod.rs
@@ -157,8 +157,89 @@ pub struct RepositoryDiff { | ||
157 | 157 | pub insertions: usize, |
158 | 158 | /// Total number of deletions |
159 | 159 | pub deletions: usize, |
160 | /// Preferably unified patch, probably a git patch. | |
161 | pub patch: String, | |
160 | /// List of changed files | |
161 | pub files: Vec<RepositoryDiffFile>, | |
162 | } | |
163 | ||
164 | /// Represents the type of change made to a [`RepositoryDiffFile`] | |
165 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
166 | pub enum RepositoryDiffFileStatus { | |
167 | /// No changes | |
168 | Unmodified, | |
169 | /// Content changed between old and new | |
170 | Modified, | |
171 | /// Renamed between old and new | |
172 | Renamed, | |
173 | /// Copied from another old entry | |
174 | Copied, | |
175 | /// Ignored item in workdir | |
176 | Ignored, | |
177 | /// Untracked item in workdir | |
178 | Untracked, | |
179 | /// Type of file changed between old and new | |
180 | Typechange, | |
181 | /// File is unreadable | |
182 | Unreadable, | |
183 | /// File in the index is conflicted | |
184 | Conflicted, | |
185 | } | |
186 | ||
187 | impl From<git2::Delta> for RepositoryDiffFileStatus { | |
188 | fn from(status: git2::Delta) -> Self { | |
189 | match status { | |
190 | git2::Delta::Unmodified => Self::Unmodified, | |
191 | git2::Delta::Modified => Self::Modified, | |
192 | git2::Delta::Renamed => Self::Renamed, | |
193 | git2::Delta::Copied => Self::Copied, | |
194 | git2::Delta::Ignored => Self::Ignored, | |
195 | git2::Delta::Untracked => Self::Untracked, | |
196 | git2::Delta::Typechange => Self::Typechange, | |
197 | git2::Delta::Unreadable => Self::Unreadable, | |
198 | git2::Delta::Conflicted => Self::Conflicted, | |
199 | _ => unreachable!(), | |
200 | } | |
201 | } | |
202 | } | |
203 | ||
204 | /// Represents a single file of a diff | |
205 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
206 | pub struct RepositoryDiffFile { | |
207 | /// The type of change made to this file | |
208 | pub status: RepositoryDiffFileStatus, | |
209 | /// "From" side of the diff, can be nonexistent if file for example got added for the first time | |
210 | pub old_file_info: Option<RepositoryDiffFileInfo>, | |
211 | /// "To" side of the diff, can be nonexistent if file got removed | |
212 | pub new_file_info: Option<RepositoryDiffFileInfo>, | |
213 | /// Individual chunks of changes in this file | |
214 | pub chunks: Vec<RepositoryDiffFileChunk>, | |
215 | } | |
216 | ||
217 | /// Represents one side of a file diff | |
218 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
219 | pub struct RepositoryDiffFileInfo { | |
220 | /// ID of the file | |
221 | pub id: String, | |
222 | /// Path of the entry relative to the working directory of the repository | |
223 | pub path: String, | |
224 | /// Size in bytes | |
225 | pub size: u64, | |
226 | /// If the file is binary or not | |
227 | pub binary: bool, | |
228 | } | |
229 | ||
230 | /// Represents a single chunk of a file diff | |
231 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
232 | pub struct RepositoryDiffFileChunk { | |
233 | /// Starting line number of the old file | |
234 | pub old_start: u32, | |
235 | /// Number of lines in "from" side of this chunk | |
236 | pub old_lines: u32, | |
237 | /// Starting line number of the new file | |
238 | pub new_start: u32, | |
239 | /// Number of lines in "to" side of this chunk | |
240 | pub new_lines: u32, | |
241 | /// Lines of the chunk | |
242 | pub lines: Vec<String>, | |
162 | 243 | } |
163 | 244 | |
164 | 245 | #[derive(Debug, Clone, Serialize, Deserialize)] |
giterated-models/src/repository/operations.rs
@@ -70,6 +70,26 @@ impl GiteratedOperation<Repository> for RepositoryDiffRequest { | ||
70 | 70 | type Failure = RepositoryError; |
71 | 71 | } |
72 | 72 | |
73 | /// A request to get the difference between two repository trees as a unified git patch. | |
74 | /// | |
75 | /// # Authentication | |
76 | /// - Instance Authentication | |
77 | /// - Validate request against the `issued_for` public key | |
78 | /// - Validate User token against the user's instance's public key | |
79 | /// # Authorization | |
80 | /// - User Authorization | |
81 | /// - Potential User permissions checks | |
82 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
83 | pub struct RepositoryDiffPatchRequest { | |
84 | pub old_id: String, | |
85 | pub new_id: String, | |
86 | } | |
87 | ||
88 | impl GiteratedOperation<Repository> for RepositoryDiffPatchRequest { | |
89 | type Success = String; | |
90 | type Failure = RepositoryError; | |
91 | } | |
92 | ||
73 | 93 | /// A request to get the commit before the one with the passed id |
74 | 94 | /// |
75 | 95 | /// # Authentication |
@@ -197,6 +217,19 @@ impl<S: Clone + Send + Sync, B: ObjectBackend<S> + std::fmt::Debug> Object<'_, S | ||
197 | 217 | .await |
198 | 218 | } |
199 | 219 | |
220 | pub async fn diff_patch( | |
221 | &mut self, | |
222 | old_id: String, | |
223 | new_id: String, | |
224 | operation_state: &S, | |
225 | ) -> Result<String, OperationError<RepositoryError>> { | |
226 | self.request::<RepositoryDiffPatchRequest>( | |
227 | RepositoryDiffPatchRequest { old_id, new_id }, | |
228 | operation_state, | |
229 | ) | |
230 | .await | |
231 | } | |
232 | ||
200 | 233 | pub async fn commit_before( |
201 | 234 | &mut self, |
202 | 235 | id: String, |