JavaScript is disabled, refresh for a better experience. ambee/giterated

ambee/giterated

Git repository hosting, collaboration, and discovery for the Fediverse.

Basic tag search and range

Emilia - ⁨1⁩ year ago

parent: tbd commit: ⁨112ca96

Showing ⁨⁨6⁩ changed files⁩ with ⁨⁨243⁩ insertions⁩ and ⁨⁨17⁩ deletions⁩

giterated-daemon/src/backend/git.rs

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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