diff --git a/giterated-daemon/src/backend/git.rs b/giterated-daemon/src/backend/git.rs index 7333cce..008b10e 100644 --- a/giterated-daemon/src/backend/git.rs +++ b/giterated-daemon/src/backend/git.rs @@ -6,16 +6,17 @@ use giterated_models::instance::{Instance, RepositoryCreateRequest}; use giterated_models::object::Object; use giterated_models::repository::{ - AccessList, BranchStaleAfter, Commit, DefaultBranch, Description, IssueLabel, Repository, - RepositoryBranch, RepositoryBranchFilter, RepositoryBranchRequest, 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, + AccessList, BranchStaleAfter, Commit, CommitSignature, DefaultBranch, Description, IssueLabel, + Repository, RepositoryBranch, RepositoryBranchFilter, RepositoryBranchRequest, + RepositoryBranchesRequest, RepositoryChunkLine, RepositoryCommitBeforeRequest, + RepositoryCommitFromIdRequest, RepositoryDiff, RepositoryDiffFile, RepositoryDiffFileChunk, + RepositoryDiffFileInfo, RepositoryDiffFileStatus, RepositoryDiffPatchRequest, + RepositoryDiffRequest, RepositoryFile, RepositoryFileFromIdRequest, + RepositoryFileFromPathRequest, RepositoryFileInspectRequest, RepositoryIssue, + RepositoryIssueLabelsRequest, RepositoryIssuesCountRequest, RepositoryIssuesRequest, + RepositoryLastCommitOfFileRequest, RepositoryObjectType, RepositoryStatistics, + RepositoryStatisticsRequest, RepositoryTag, RepositoryTagsRequest, RepositoryTreeEntry, + RepositoryVisibility, Visibility, }; use giterated_models::user::User; @@ -971,6 +972,131 @@ impl RepositoryBackend for GitBackend { }) } + /// .0: List of tags in passed range + /// .1: Total amount of tags + async fn repository_get_tags( + &mut self, + requester: &Option, + repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, + OperationState(_operation_state): OperationState, + request: &RepositoryTagsRequest, + ) -> Result<(Vec, usize), Error> { + let repository: &Repository = repository_object.object(); + let git = self + .open_repository_and_check_permissions(&repository.owner, &repository.name, requester) + .await?; + + let mut tags = vec![]; + + // Iterate over each tag + let _ = git.tag_foreach(|id, name| { + // Get the name in utf8 + let name = String::from_utf8_lossy(name).replacen("refs/tags/", "", 1); + + // Find the tag so we can get the messages attached if any + if let Ok(tag) = git.find_tag(id) { + // Get the tag message and split it into a summary and body + let (summary, body) = if let Some(message) = tag.message() { + // Iterate over the lines + let mut lines = message + .lines() + .map(|line| { + // Trim the whitespace for every line + let mut whitespace_removed = String::with_capacity(line.len()); + + line.split_whitespace().for_each(|word| { + if !whitespace_removed.is_empty() { + whitespace_removed.push(' '); + } + + whitespace_removed.push_str(word); + }); + + whitespace_removed + }) + .collect::>(); + + let summary = Some(lines.remove(0)); + let body = if lines.is_empty() { + None + } else { + Some(lines.join("\n")) + }; + + (summary, body) + } else { + (None, None) + }; + + // Get the commit the tag is (possibly) pointing to + let commit = tag + .peel() + .map(|obj| obj.into_commit().ok()) + .ok() + .flatten() + .map(|c| Commit::from(c)); + // Get the author of the tag + let author: Option = tag.tagger().map(|s| s.into()); + // Get the time the tag or pointed commit was created + let time = if let Some(ref author) = author { + Some(author.time) + } else { + // Get possible commit time if the tag has no author time + commit.as_ref().map(|c| c.time.clone()) + }; + + tags.push(RepositoryTag { + id: id.to_string(), + name: name.to_string(), + summary, + body, + author, + time, + commit, + }); + } else { + // Lightweight commit, we try and find the commit it's pointing to + let commit = git.find_commit(id).ok().map(|c| Commit::from(c)); + + tags.push(RepositoryTag { + id: id.to_string(), + name: name.to_string(), + summary: None, + body: None, + author: None, + time: commit.as_ref().map(|c| c.time.clone()), + commit, + }); + }; + + true + }); + + // Get the total amount of tags + let tag_count = tags.len(); + + if let Some(search) = &request.search { + // TODO: Caching + // Search by sorting using a simple fuzzy search algorithm + tags.sort_by(|n1, n2| { + strsim::damerau_levenshtein(search, &n1.name) + .cmp(&strsim::damerau_levenshtein(search, &n2.name)) + }); + } else { + // Sort the tags using their creation or pointer date + tags.sort_by(|t1, t2| t2.time.cmp(&t1.time)); + } + + // Get the requested range of tags + let tags = tags + .into_iter() + .skip(request.range.0) + .take(request.range.1.saturating_sub(request.range.0)) + .collect::>(); + + Ok((tags, tag_count)) + } + async fn repository_diff( &mut self, requester: &Option, diff --git a/giterated-daemon/src/backend/mod.rs b/giterated-daemon/src/backend/mod.rs index 999728d..907c363 100644 --- a/giterated-daemon/src/backend/mod.rs +++ b/giterated-daemon/src/backend/mod.rs @@ -24,7 +24,8 @@ use giterated_models::repository::{ RepositoryFileFromIdRequest, RepositoryFileFromPathRequest, RepositoryFileInspectRequest, RepositoryIssue, RepositoryIssueLabelsRequest, RepositoryIssuesCountRequest, RepositoryIssuesRequest, RepositoryLastCommitOfFileRequest, RepositoryStatistics, - RepositoryStatisticsRequest, RepositorySummary, RepositoryTreeEntry, + RepositoryStatisticsRequest, RepositorySummary, RepositoryTag, RepositoryTagsRequest, + RepositoryTreeEntry, }; use giterated_models::user::User; @@ -104,6 +105,13 @@ pub trait RepositoryBackend { OperationState(operation_state): OperationState, request: &RepositoryBranchRequest, ) -> Result; + async fn repository_get_tags( + &mut self, + requester: &Option, + repository_object: &mut Object<'_, StackOperationState, Repository, GiteratedStack>, + OperationState(operation_state): OperationState, + request: &RepositoryTagsRequest, + ) -> Result<(Vec, usize), Error>; async fn exists( &mut self, requester: &Option, diff --git a/giterated-daemon/src/database_backend/handler.rs b/giterated-daemon/src/database_backend/handler.rs index c9d2639..968791e 100644 --- a/giterated-daemon/src/database_backend/handler.rs +++ b/giterated-daemon/src/database_backend/handler.rs @@ -12,7 +12,7 @@ use giterated_models::{ RepositoryDiffRequest, RepositoryFile, RepositoryFileFromIdRequest, RepositoryFileFromPathRequest, RepositoryFileInspectRequest, RepositoryInfoRequest, RepositoryLastCommitOfFileRequest, RepositoryStatistics, RepositoryStatisticsRequest, - RepositorySummary, RepositoryView, Visibility, + RepositorySummary, RepositoryTag, RepositoryTagsRequest, RepositoryView, Visibility, }, user::{User, UserRepositoriesRequest}, }; @@ -198,6 +198,34 @@ pub async fn repository_get_branch( Ok(branch) } +pub async fn repository_get_tags( + object: Repository, + operation: RepositoryTagsRequest, + state: DatabaseBackend, + OperationState(operation_state): OperationState, + backend: GiteratedStack, + requester: Option, +) -> Result<(Vec, usize), OperationError> { + let mut object = backend + .get_object::(&object.to_string(), &operation_state) + .await + .unwrap(); + + let mut repository_backend = state.repository_backend.lock().await; + let branches = repository_backend + .repository_get_tags( + &requester, + &mut object, + OperationState(operation_state), + &operation, + ) + .await + .as_internal_error()?; + drop(repository_backend); + + Ok(branches) +} + pub async fn repository_file_from_id( object: Repository, operation: RepositoryFileFromIdRequest, diff --git a/giterated-daemon/src/database_backend/mod.rs b/giterated-daemon/src/database_backend/mod.rs index 10b2043..3e19a7f 100644 --- a/giterated-daemon/src/database_backend/mod.rs +++ b/giterated-daemon/src/database_backend/mod.rs @@ -26,8 +26,8 @@ use self::handler::{ instance_authentication_request, instance_create_repository_request, instance_registration_request, repository_commit_before, repository_commit_by_id, repository_diff, repository_diff_patch, repository_file_from_id, repository_file_from_path, - repository_get_branch, repository_get_branches, repository_get_statistics, repository_info, - repository_last_commit_of_file, user_get_repositories, + repository_get_branch, repository_get_branches, repository_get_statistics, repository_get_tags, + repository_info, repository_last_commit_of_file, user_get_repositories, }; /// A backend implementation which attempts to resolve data from the instance's database. @@ -96,7 +96,8 @@ impl DatabaseBackend { .operation(repository_diff_patch) .operation(repository_commit_before) .operation(repository_get_branches) - .operation(repository_get_branch); + .operation(repository_get_branch) + .operation(repository_get_tags); builder } diff --git a/giterated-models/src/repository/mod.rs b/giterated-models/src/repository/mod.rs index dc916a9..5573e31 100644 --- a/giterated-models/src/repository/mod.rs +++ b/giterated-models/src/repository/mod.rs @@ -1,6 +1,7 @@ use std::fmt::{Display, Formatter}; use std::str::FromStr; +use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; use crate::object::GiteratedObject; @@ -159,7 +160,7 @@ pub struct RepositoryStatistics { pub struct RepositoryBranch { /// Full reference name pub name: String, - /// Whether the repository is stale or not + /// Whether the branch is stale or not pub stale: bool, /// The last commit made to the branch pub last_commit: Option, @@ -175,6 +176,25 @@ pub enum RepositoryBranchFilter { Stale, } +/// Repository tag +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RepositoryTag { + /// Git OID for the tag + pub id: String, + /// Full tag name + pub name: String, + /// First paragraph of the full message + pub summary: Option, + /// Everything in the full message apart from the first paragraph + pub body: Option, + /// Signature of the tag author + pub author: Option, + /// Time the tag or pointed commit was created + pub time: Option, + /// Commit the tag is pointing to + pub commit: Option, +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RepositoryFile { /// ID of the file diff --git a/giterated-models/src/repository/operations.rs b/giterated-models/src/repository/operations.rs index b42ab26..2320769 100644 --- a/giterated-models/src/repository/operations.rs +++ b/giterated-models/src/repository/operations.rs @@ -9,7 +9,8 @@ use crate::{ use super::{ Commit, IssueLabel, Repository, RepositoryBranch, RepositoryBranchFilter, RepositoryDiff, - RepositoryFile, RepositoryIssue, RepositoryStatistics, RepositoryTreeEntry, RepositoryView, + RepositoryFile, RepositoryIssue, RepositoryStatistics, RepositoryTag, RepositoryTreeEntry, + RepositoryView, }; /// A request to get a repository's information. @@ -299,6 +300,31 @@ impl GiteratedOperation for RepositoryBranchRequest { type Failure = RepositoryError; } +/// A request to get a list of tags in the repository. +/// Also returns the total amount of tags. +/// +/// Optional search parameter that'll search through the tags +/// +/// # 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 RepositoryTagsRequest { + // pub filter: Option, + pub range: (usize, usize), + // pub sort: Option, + pub search: Option, +} + +impl GiteratedOperation for RepositoryTagsRequest { + type Success = (Vec, usize); + type Failure = RepositoryError; +} + impl + std::fmt::Debug> Object<'_, S, Repository, B> { pub async fn info( &mut self, @@ -451,6 +477,23 @@ impl + std::fmt::Debug> Object<'_, S .await } + pub async fn tags( + &mut self, + range_start: usize, + range_end: usize, + search: Option, + operation_state: &S, + ) -> Result<(Vec, usize), OperationError> { + self.request::( + RepositoryTagsRequest { + range: (range_start, range_end), + search, + }, + operation_state, + ) + .await + } + // pub async fn issues_count(&mut self) -> Result> { // self.request::(RepositoryIssuesCountRequest) // .await