Basic tag search and range
parent: tbd commit: 112ca96
Showing 6 changed files with 243 insertions and 17 deletions
giterated-daemon/src/backend/git.rs
@@ -6,16 +6,17 @@ use giterated_models::instance::{Instance, RepositoryCreateRequest}; | ||
6 | 6 | |
7 | 7 | use giterated_models::object::Object; |
8 | 8 | use giterated_models::repository::{ |
9 | AccessList, BranchStaleAfter, Commit, DefaultBranch, Description, IssueLabel, Repository, | |
10 | RepositoryBranch, RepositoryBranchFilter, RepositoryBranchRequest, RepositoryBranchesRequest, | |
11 | RepositoryChunkLine, RepositoryCommitBeforeRequest, RepositoryCommitFromIdRequest, | |
12 | RepositoryDiff, RepositoryDiffFile, RepositoryDiffFileChunk, RepositoryDiffFileInfo, | |
13 | RepositoryDiffFileStatus, RepositoryDiffPatchRequest, RepositoryDiffRequest, RepositoryFile, | |
14 | RepositoryFileFromIdRequest, RepositoryFileFromPathRequest, RepositoryFileInspectRequest, | |
15 | RepositoryIssue, RepositoryIssueLabelsRequest, RepositoryIssuesCountRequest, | |
16 | RepositoryIssuesRequest, RepositoryLastCommitOfFileRequest, RepositoryObjectType, | |
17 | RepositoryStatistics, RepositoryStatisticsRequest, RepositoryTreeEntry, RepositoryVisibility, | |
18 | Visibility, | |
9 | AccessList, BranchStaleAfter, Commit, CommitSignature, DefaultBranch, Description, IssueLabel, | |
10 | Repository, RepositoryBranch, RepositoryBranchFilter, RepositoryBranchRequest, | |
11 | RepositoryBranchesRequest, RepositoryChunkLine, RepositoryCommitBeforeRequest, | |
12 | RepositoryCommitFromIdRequest, RepositoryDiff, RepositoryDiffFile, RepositoryDiffFileChunk, | |
13 | RepositoryDiffFileInfo, RepositoryDiffFileStatus, RepositoryDiffPatchRequest, | |
14 | RepositoryDiffRequest, RepositoryFile, RepositoryFileFromIdRequest, | |
15 | RepositoryFileFromPathRequest, RepositoryFileInspectRequest, RepositoryIssue, | |
16 | RepositoryIssueLabelsRequest, RepositoryIssuesCountRequest, RepositoryIssuesRequest, | |
17 | RepositoryLastCommitOfFileRequest, RepositoryObjectType, RepositoryStatistics, | |
18 | RepositoryStatisticsRequest, RepositoryTag, RepositoryTagsRequest, RepositoryTreeEntry, | |
19 | RepositoryVisibility, Visibility, | |
19 | 20 | }; |
20 | 21 | |
21 | 22 | use giterated_models::user::User; |
@@ -971,6 +972,131 @@ impl RepositoryBackend for GitBackend { | ||
971 | 972 | }) |
972 | 973 | } |
973 | 974 | |
975 | /// .0: List of tags in passed range | |
976 | /// .1: Total amount of tags | |
977 | async fn repository_get_tags( | |
978 | &mut self, | |
979 | requester: &Option<AuthenticatedUser>, | |
980 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
981 | OperationState(_operation_state): OperationState<StackOperationState>, | |
982 | request: &RepositoryTagsRequest, | |
983 | ) -> Result<(Vec<RepositoryTag>, usize), Error> { | |
984 | let repository: &Repository = repository_object.object(); | |
985 | let git = self | |
986 | .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) | |
987 | .await?; | |
988 | ||
989 | let mut tags = vec![]; | |
990 | ||
991 | // Iterate over each tag | |
992 | let _ = git.tag_foreach(|id, name| { | |
993 | // Get the name in utf8 | |
994 | let name = String::from_utf8_lossy(name).replacen("refs/tags/", "", 1); | |
995 | ||
996 | // Find the tag so we can get the messages attached if any | |
997 | if let Ok(tag) = git.find_tag(id) { | |
998 | // Get the tag message and split it into a summary and body | |
999 | let (summary, body) = if let Some(message) = tag.message() { | |
1000 | // Iterate over the lines | |
1001 | let mut lines = message | |
1002 | .lines() | |
1003 | .map(|line| { | |
1004 | // Trim the whitespace for every line | |
1005 | let mut whitespace_removed = String::with_capacity(line.len()); | |
1006 | ||
1007 | line.split_whitespace().for_each(|word| { | |
1008 | if !whitespace_removed.is_empty() { | |
1009 | whitespace_removed.push(' '); | |
1010 | } | |
1011 | ||
1012 | whitespace_removed.push_str(word); | |
1013 | }); | |
1014 | ||
1015 | whitespace_removed | |
1016 | }) | |
1017 | .collect::<Vec<String>>(); | |
1018 | ||
1019 | let summary = Some(lines.remove(0)); | |
1020 | let body = if lines.is_empty() { | |
1021 | None | |
1022 | } else { | |
1023 | Some(lines.join("\n")) | |
1024 | }; | |
1025 | ||
1026 | (summary, body) | |
1027 | } else { | |
1028 | (None, None) | |
1029 | }; | |
1030 | ||
1031 | // Get the commit the tag is (possibly) pointing to | |
1032 | let commit = tag | |
1033 | .peel() | |
1034 | .map(|obj| obj.into_commit().ok()) | |
1035 | .ok() | |
1036 | .flatten() | |
1037 | .map(|c| Commit::from(c)); | |
1038 | // Get the author of the tag | |
1039 | let author: Option<CommitSignature> = tag.tagger().map(|s| s.into()); | |
1040 | // Get the time the tag or pointed commit was created | |
1041 | let time = if let Some(ref author) = author { | |
1042 | Some(author.time) | |
1043 | } else { | |
1044 | // Get possible commit time if the tag has no author time | |
1045 | commit.as_ref().map(|c| c.time.clone()) | |
1046 | }; | |
1047 | ||
1048 | tags.push(RepositoryTag { | |
1049 | id: id.to_string(), | |
1050 | name: name.to_string(), | |
1051 | summary, | |
1052 | body, | |
1053 | author, | |
1054 | time, | |
1055 | commit, | |
1056 | }); | |
1057 | } else { | |
1058 | // Lightweight commit, we try and find the commit it's pointing to | |
1059 | let commit = git.find_commit(id).ok().map(|c| Commit::from(c)); | |
1060 | ||
1061 | tags.push(RepositoryTag { | |
1062 | id: id.to_string(), | |
1063 | name: name.to_string(), | |
1064 | summary: None, | |
1065 | body: None, | |
1066 | author: None, | |
1067 | time: commit.as_ref().map(|c| c.time.clone()), | |
1068 | commit, | |
1069 | }); | |
1070 | }; | |
1071 | ||
1072 | true | |
1073 | }); | |
1074 | ||
1075 | // Get the total amount of tags | |
1076 | let tag_count = tags.len(); | |
1077 | ||
1078 | if let Some(search) = &request.search { | |
1079 | // TODO: Caching | |
1080 | // Search by sorting using a simple fuzzy search algorithm | |
1081 | tags.sort_by(|n1, n2| { | |
1082 | strsim::damerau_levenshtein(search, &n1.name) | |
1083 | .cmp(&strsim::damerau_levenshtein(search, &n2.name)) | |
1084 | }); | |
1085 | } else { | |
1086 | // Sort the tags using their creation or pointer date | |
1087 | tags.sort_by(|t1, t2| t2.time.cmp(&t1.time)); | |
1088 | } | |
1089 | ||
1090 | // Get the requested range of tags | |
1091 | let tags = tags | |
1092 | .into_iter() | |
1093 | .skip(request.range.0) | |
1094 | .take(request.range.1.saturating_sub(request.range.0)) | |
1095 | .collect::<Vec<RepositoryTag>>(); | |
1096 | ||
1097 | Ok((tags, tag_count)) | |
1098 | } | |
1099 | ||
974 | 1100 | async fn repository_diff( |
975 | 1101 | &mut self, |
976 | 1102 | requester: &Option<AuthenticatedUser>, |
giterated-daemon/src/backend/mod.rs
@@ -24,7 +24,8 @@ use giterated_models::repository::{ | ||
24 | 24 | RepositoryFileFromIdRequest, RepositoryFileFromPathRequest, RepositoryFileInspectRequest, |
25 | 25 | RepositoryIssue, RepositoryIssueLabelsRequest, RepositoryIssuesCountRequest, |
26 | 26 | RepositoryIssuesRequest, RepositoryLastCommitOfFileRequest, RepositoryStatistics, |
27 | RepositoryStatisticsRequest, RepositorySummary, RepositoryTreeEntry, | |
27 | RepositoryStatisticsRequest, RepositorySummary, RepositoryTag, RepositoryTagsRequest, | |
28 | RepositoryTreeEntry, | |
28 | 29 | }; |
29 | 30 | |
30 | 31 | use giterated_models::user::User; |
@@ -104,6 +105,13 @@ pub trait RepositoryBackend { | ||
104 | 105 | OperationState(operation_state): OperationState<StackOperationState>, |
105 | 106 | request: &RepositoryBranchRequest, |
106 | 107 | ) -> Result<RepositoryBranch, Error>; |
108 | async fn repository_get_tags( | |
109 | &mut self, | |
110 | requester: &Option<AuthenticatedUser>, | |
111 | repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, | |
112 | OperationState(operation_state): OperationState<StackOperationState>, | |
113 | request: &RepositoryTagsRequest, | |
114 | ) -> Result<(Vec<RepositoryTag>, usize), Error>; | |
107 | 115 | async fn exists( |
108 | 116 | &mut self, |
109 | 117 | requester: &Option<AuthenticatedUser>, |
giterated-daemon/src/database_backend/handler.rs
@@ -12,7 +12,7 @@ use giterated_models::{ | ||
12 | 12 | RepositoryDiffRequest, RepositoryFile, RepositoryFileFromIdRequest, |
13 | 13 | RepositoryFileFromPathRequest, RepositoryFileInspectRequest, RepositoryInfoRequest, |
14 | 14 | RepositoryLastCommitOfFileRequest, RepositoryStatistics, RepositoryStatisticsRequest, |
15 | RepositorySummary, RepositoryView, Visibility, | |
15 | RepositorySummary, RepositoryTag, RepositoryTagsRequest, RepositoryView, Visibility, | |
16 | 16 | }, |
17 | 17 | user::{User, UserRepositoriesRequest}, |
18 | 18 | }; |
@@ -198,6 +198,34 @@ pub async fn repository_get_branch( | ||
198 | 198 | Ok(branch) |
199 | 199 | } |
200 | 200 | |
201 | pub async fn repository_get_tags( | |
202 | object: Repository, | |
203 | operation: RepositoryTagsRequest, | |
204 | state: DatabaseBackend, | |
205 | OperationState(operation_state): OperationState<StackOperationState>, | |
206 | backend: GiteratedStack, | |
207 | requester: Option<AuthenticatedUser>, | |
208 | ) -> Result<(Vec<RepositoryTag>, usize), OperationError<RepositoryError>> { | |
209 | let mut object = backend | |
210 | .get_object::<Repository>(&object.to_string(), &operation_state) | |
211 | .await | |
212 | .unwrap(); | |
213 | ||
214 | let mut repository_backend = state.repository_backend.lock().await; | |
215 | let branches = repository_backend | |
216 | .repository_get_tags( | |
217 | &requester, | |
218 | &mut object, | |
219 | OperationState(operation_state), | |
220 | &operation, | |
221 | ) | |
222 | .await | |
223 | .as_internal_error()?; | |
224 | drop(repository_backend); | |
225 | ||
226 | Ok(branches) | |
227 | } | |
228 | ||
201 | 229 | pub async fn repository_file_from_id( |
202 | 230 | object: Repository, |
203 | 231 | operation: RepositoryFileFromIdRequest, |
giterated-daemon/src/database_backend/mod.rs
@@ -26,8 +26,8 @@ use self::handler::{ | ||
26 | 26 | instance_authentication_request, instance_create_repository_request, |
27 | 27 | instance_registration_request, repository_commit_before, repository_commit_by_id, |
28 | 28 | repository_diff, repository_diff_patch, repository_file_from_id, repository_file_from_path, |
29 | repository_get_branch, repository_get_branches, repository_get_statistics, repository_info, | |
30 | repository_last_commit_of_file, user_get_repositories, | |
29 | repository_get_branch, repository_get_branches, repository_get_statistics, repository_get_tags, | |
30 | repository_info, repository_last_commit_of_file, user_get_repositories, | |
31 | 31 | }; |
32 | 32 | |
33 | 33 | /// A backend implementation which attempts to resolve data from the instance's database. |
@@ -96,7 +96,8 @@ impl DatabaseBackend { | ||
96 | 96 | .operation(repository_diff_patch) |
97 | 97 | .operation(repository_commit_before) |
98 | 98 | .operation(repository_get_branches) |
99 | .operation(repository_get_branch); | |
99 | .operation(repository_get_branch) | |
100 | .operation(repository_get_tags); | |
100 | 101 | |
101 | 102 | builder |
102 | 103 | } |
giterated-models/src/repository/mod.rs
@@ -1,6 +1,7 @@ | ||
1 | 1 | use std::fmt::{Display, Formatter}; |
2 | 2 | use std::str::FromStr; |
3 | 3 | |
4 | use chrono::NaiveDateTime; | |
4 | 5 | use serde::{Deserialize, Serialize}; |
5 | 6 | |
6 | 7 | use crate::object::GiteratedObject; |
@@ -159,7 +160,7 @@ pub struct RepositoryStatistics { | ||
159 | 160 | pub struct RepositoryBranch { |
160 | 161 | /// Full reference name |
161 | 162 | pub name: String, |
162 | /// Whether the repository is stale or not | |
163 | /// Whether the branch is stale or not | |
163 | 164 | pub stale: bool, |
164 | 165 | /// The last commit made to the branch |
165 | 166 | pub last_commit: Option<Commit>, |
@@ -175,6 +176,25 @@ pub enum RepositoryBranchFilter { | ||
175 | 176 | Stale, |
176 | 177 | } |
177 | 178 | |
179 | /// Repository tag | |
180 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
181 | pub struct RepositoryTag { | |
182 | /// Git OID for the tag | |
183 | pub id: String, | |
184 | /// Full tag name | |
185 | pub name: String, | |
186 | /// First paragraph of the full message | |
187 | pub summary: Option<String>, | |
188 | /// Everything in the full message apart from the first paragraph | |
189 | pub body: Option<String>, | |
190 | /// Signature of the tag author | |
191 | pub author: Option<CommitSignature>, | |
192 | /// Time the tag or pointed commit was created | |
193 | pub time: Option<NaiveDateTime>, | |
194 | /// Commit the tag is pointing to | |
195 | pub commit: Option<Commit>, | |
196 | } | |
197 | ||
178 | 198 | #[derive(Clone, Debug, Serialize, Deserialize)] |
179 | 199 | pub struct RepositoryFile { |
180 | 200 | /// ID of the file |
giterated-models/src/repository/operations.rs
@@ -9,7 +9,8 @@ use crate::{ | ||
9 | 9 | |
10 | 10 | use super::{ |
11 | 11 | Commit, IssueLabel, Repository, RepositoryBranch, RepositoryBranchFilter, RepositoryDiff, |
12 | RepositoryFile, RepositoryIssue, RepositoryStatistics, RepositoryTreeEntry, RepositoryView, | |
12 | RepositoryFile, RepositoryIssue, RepositoryStatistics, RepositoryTag, RepositoryTreeEntry, | |
13 | RepositoryView, | |
13 | 14 | }; |
14 | 15 | |
15 | 16 | /// A request to get a repository's information. |
@@ -299,6 +300,31 @@ impl GiteratedOperation<Repository> for RepositoryBranchRequest { | ||
299 | 300 | type Failure = RepositoryError; |
300 | 301 | } |
301 | 302 | |
303 | /// A request to get a list of tags in the repository. | |
304 | /// Also returns the total amount of tags. | |
305 | /// | |
306 | /// Optional search parameter that'll search through the tags | |
307 | /// | |
308 | /// # Authentication | |
309 | /// - Instance Authentication | |
310 | /// - Validate request against the `issued_for` public key | |
311 | /// - Validate User token against the user's instance's public key | |
312 | /// # Authorization | |
313 | /// - User Authorization | |
314 | /// - Potential User permissions checks | |
315 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
316 | pub struct RepositoryTagsRequest { | |
317 | // pub filter: Option<???>, | |
318 | pub range: (usize, usize), | |
319 | // pub sort: Option<???>, | |
320 | pub search: Option<String>, | |
321 | } | |
322 | ||
323 | impl GiteratedOperation<Repository> for RepositoryTagsRequest { | |
324 | type Success = (Vec<RepositoryTag>, usize); | |
325 | type Failure = RepositoryError; | |
326 | } | |
327 | ||
302 | 328 | impl<S: Clone + Send + Sync, B: ObjectBackend<S> + std::fmt::Debug> Object<'_, S, Repository, B> { |
303 | 329 | pub async fn info( |
304 | 330 | &mut self, |
@@ -451,6 +477,23 @@ impl<S: Clone + Send + Sync, B: ObjectBackend<S> + std::fmt::Debug> Object<'_, S | ||
451 | 477 | .await |
452 | 478 | } |
453 | 479 | |
480 | pub async fn tags( | |
481 | &mut self, | |
482 | range_start: usize, | |
483 | range_end: usize, | |
484 | search: Option<String>, | |
485 | operation_state: &S, | |
486 | ) -> Result<(Vec<RepositoryTag>, usize), OperationError<RepositoryError>> { | |
487 | self.request::<RepositoryTagsRequest>( | |
488 | RepositoryTagsRequest { | |
489 | range: (range_start, range_end), | |
490 | search, | |
491 | }, | |
492 | operation_state, | |
493 | ) | |
494 | .await | |
495 | } | |
496 | ||
454 | 497 | // pub async fn issues_count(&mut self) -> Result<u64, OperationError<RepositoryError>> { |
455 | 498 | // self.request::<RepositoryIssuesCountRequest>(RepositoryIssuesCountRequest) |
456 | 499 | // .await |