diff --git a/Cargo.toml b/Cargo.toml index 0786dd3..6707e4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,4 +19,4 @@ pedantic = "deny" nursery = "deny" unwrap_used = "deny" missing_errors_doc = "allow" -module_name_repetitions = "allow" \ No newline at end of file +module_name_repetitions = "allow" diff --git a/skootrs-bin/src/helpers.rs b/skootrs-bin/src/helpers.rs index 101383f..4d083f6 100644 --- a/skootrs-bin/src/helpers.rs +++ b/skootrs-bin/src/helpers.rs @@ -8,9 +8,9 @@ use skootrs_model::{ skootrs::{ facet::InitializedFacet, Config, EcosystemInitializeParams, FacetGetParams, FacetMapKey, GithubRepoParams, GithubUser, GoParams, InitializedProject, MavenParams, - ProjectCreateParams, ProjectGetParams, ProjectOutputParams, ProjectOutputReference, - ProjectOutputType, ProjectOutputsListParams, ProjectReleaseParam, RepoCreateParams, - SkootError, SourceInitializeParams, SupportedEcosystems, + ProjectArchiveParams, ProjectCreateParams, ProjectGetParams, ProjectOutputParams, + ProjectOutputReference, ProjectOutputType, ProjectOutputsListParams, ProjectReleaseParam, + RepoCreateParams, SkootError, SourceInitializeParams, SupportedEcosystems, }, }; use std::{collections::HashSet, io::Write, str::FromStr}; @@ -160,8 +160,8 @@ impl Project { Ok(project) } - async fn prompt_get(_config: &Config) -> Result { - let projects = Project::list().await?; + async fn prompt_get(config: &Config) -> Result { + let projects = Project::list(config).await?; let selected_project = inquire::Select::new("Select a project", projects.iter().collect()).prompt()?; Ok(ProjectGetParams { @@ -174,11 +174,35 @@ impl Project { /// # Errors /// /// Returns an error if the cache can't be loaded or if the list of projects can't be fetched. - pub async fn list() -> Result, SkootError> { + pub async fn list(_config: &Config) -> Result, SkootError> { let cache = InMemoryProjectReferenceCache::load_or_create("./skootcache")?; let projects: HashSet = cache.list().await?; Ok(projects) } + + /// Archives a project by archiving the repository and removing it from the local cache. + /// + /// # Errors + /// + /// Returns an error if the project can't be archived or deleted from the cache. + pub async fn archive<'a, T: ProjectService + ?Sized>( + config: &Config, + project_service: &'a T, + project_archive_params: Option, + ) -> Result<(), SkootError> { + let project_archive_params = match project_archive_params { + Some(p) => p, + None => ProjectArchiveParams { + initialized_project: Project::get(config, project_service, None).await?, + }, + }; + let url = project_archive_params.initialized_project.repo.full_url(); + project_service.archive(project_archive_params).await?; + let mut local_cache = InMemoryProjectReferenceCache::load_or_create("./skootcache")?; + local_cache.delete(url).await?; + local_cache.save()?; + Ok(()) + } } pub struct Facet; @@ -295,8 +319,8 @@ impl Output { Ok(output_list) } - async fn prompt_project_output(_config: &Config) -> Result { - let projects = Project::list().await?; + async fn prompt_project_output(config: &Config) -> Result { + let projects = Project::list(config).await?; let selected_project = inquire::Select::new("Select a project", projects.iter().collect()).prompt()?; let selected_output_type = diff --git a/skootrs-bin/src/main.rs b/skootrs-bin/src/main.rs index 257fa34..d551768 100644 --- a/skootrs-bin/src/main.rs +++ b/skootrs-bin/src/main.rs @@ -108,6 +108,16 @@ enum ProjectCommands { #[clap(value_parser)] input: Option, }, + + /// Archive a project. + #[command(name = "archive")] + Archive { + /// This is an optional input parameter that can be used to pass in a file, pipe, url, or stdin. + /// This is expected to be YAML or JSON. If it is not provided, the CLI will prompt the user for the input. + #[clap(value_parser)] + input: Option, + }, + /// List all the projects known to the local Skootrs #[command(name = "list")] List, @@ -222,6 +232,7 @@ fn parse_optional_input( } } +#[allow(clippy::too_many_lines)] #[tokio::main] async fn main() -> std::result::Result<(), SkootError> { init_tracing(); @@ -260,13 +271,22 @@ async fn main() -> std::result::Result<(), SkootError> { } } ProjectCommands::List => { - if let Err(ref error) = helpers::Project::list() + if let Err(ref error) = helpers::Project::list(&config) .await .handle_response_output(stdout()) { error!(error = error.as_ref(), "Failed to list projects"); } } + ProjectCommands::Archive { input } => { + let project_archive_params = parse_optional_input(input)?; + if let Err(ref error) = + helpers::Project::archive(&config, &project_service, project_archive_params) + .await + { + error!(error = error.as_ref(), "Failed to archive project"); + } + } }, SkootrsCli::Facet { facet } => match facet { FacetCommands::Get { input } => { diff --git a/skootrs-lib/src/service/project.rs b/skootrs-lib/src/service/project.rs index 752e896..7129d3a 100644 --- a/skootrs-lib/src/service/project.rs +++ b/skootrs-lib/src/service/project.rs @@ -21,8 +21,9 @@ use crate::service::facet::{FacetSetParamsGenerator, RootFacetService}; use skootrs_model::skootrs::{ facet::{CommonFacetCreateParams, InitializedFacet, SourceFile}, - FacetGetParams, FacetMapKey, InitializedProject, InitializedSource, ProjectCreateParams, - ProjectGetParams, ProjectOutputReference, ProjectOutputsListParams, SkootError, + FacetGetParams, FacetMapKey, InitializedProject, InitializedSource, ProjectArchiveParams, + ProjectCreateParams, ProjectGetParams, ProjectOutputReference, ProjectOutputsListParams, + SkootError, }; use super::{ @@ -72,10 +73,25 @@ pub trait ProjectService { params: ProjectGetParams, ) -> impl std::future::Future, SkootError>> + Send; + /// Lists the outputs of an initialized project. + /// + /// # Errors + /// + /// Returns an error if the list of outputs can't be fetched. fn outputs_list( &self, params: ProjectOutputsListParams, ) -> impl std::future::Future, SkootError>> + Send; + + /// Archives an initialized project. + /// + /// # Errors + /// + /// Returns an error if the project can't be archived. + fn archive( + &self, + _params: ProjectArchiveParams, + ) -> impl std::future::Future> + Send; } /// The `LocalProjectService` struct provides an implementation of the `ProjectService` trait for initializing @@ -245,6 +261,12 @@ where async fn list_facets(&self, params: ProjectGetParams) -> Result, SkootError> { Ok(self.get(params).await?.facets.keys().cloned().collect()) } + + async fn archive(&self, params: ProjectArchiveParams) -> Result { + self.repo_service + .archive(params.initialized_project.repo) + .await + } } #[cfg(test)] @@ -342,6 +364,10 @@ mod tests { Ok("Worked".to_string()) } + + async fn archive(&self, initialized_repo: InitializedRepo) -> Result { + Ok(initialized_repo.full_url()) + } } impl EcosystemService for MockEcosystemService { diff --git a/skootrs-lib/src/service/repo.rs b/skootrs-lib/src/service/repo.rs index 9363381..07fba41 100644 --- a/skootrs-lib/src/service/repo.rs +++ b/skootrs-lib/src/service/repo.rs @@ -19,6 +19,7 @@ use std::{process::Command, str::FromStr, sync::Arc}; use chrono::Utc; +use octocrab::Octocrab; use tracing::{info, debug}; use skootrs_model::{cd_events::repo_created::{RepositoryCreatedEvent, RepositoryCreatedEventContext, RepositoryCreatedEventContextId, RepositoryCreatedEventContextVersion, RepositoryCreatedEventSubject, RepositoryCreatedEventSubjectContent, RepositoryCreatedEventSubjectContentName, RepositoryCreatedEventSubjectContentUrl, RepositoryCreatedEventSubjectId}, skootrs::{InitializedRepoGetParams, GithubRepoParams, GithubUser, InitializedGithubRepo, InitializedRepo, InitializedSource, RepoCreateParams, SkootError}}; @@ -62,6 +63,8 @@ pub trait RepoService { /// /// Returns an error if the file can't be fetched from the repository for any reason. fn fetch_file_content + Send>(&self, initialized_repo: &InitializedRepo, path: P) -> impl std::future::Future> + std::marker::Send; + + fn archive(&self, initialized_repo: InitializedRepo) -> impl std::future::Future> + Send; } /// The `LocalRepoService` struct provides an implementation of the `RepoService` trait for initializing @@ -171,6 +174,32 @@ impl RepoService for LocalRepoService { } } } + + async fn archive(&self, initialized_repo: InitializedRepo) -> Result { + match initialized_repo { + InitializedRepo::Github(g) => { + #[derive(serde::Serialize)] + struct ArchiveParams { + archived: bool, + } + let owner = g.organization.get_name(); + let repo = g.name.clone(); + let body = ArchiveParams { + archived: true, + }; + + info!("Archiving {owner}/{repo}"); + + // FIXME: This should work with `Octocrabe::instance()` but for some reason it doesn't pick up the token/session + let token = std::env::var("GITHUB_TOKEN").expect("GITHUB_TOKEN env variable is required"); + let octocrab = Octocrab::builder().personal_token(token).build()?; + let archived_response: serde_json::Value = octocrab.patch(format!("/repos/{owner}/{repo}"), Some(&body)).await?; + info!("Archived: {archived_response}"); + + Ok(g.full_url()) + } + } + } } /// The `GithubRepoHandler` struct represents a handler for initializing and managing Github repos. diff --git a/skootrs-model/src/skootrs/mod.rs b/skootrs-model/src/skootrs/mod.rs index 820364a..995e226 100644 --- a/skootrs-model/src/skootrs/mod.rs +++ b/skootrs-model/src/skootrs/mod.rs @@ -194,6 +194,14 @@ pub struct ProjectOutputParams { pub project_output: String, } +/// The parameters for archiving a project. +#[derive(Serialize, Deserialize, Clone, Debug)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub struct ProjectArchiveParams { + /// The initialized project to archive. + pub initialized_project: InitializedProject, +} + /// The set of supported output types #[derive(Serialize, Deserialize, Clone, Debug)] #[cfg_attr(feature = "openapi", derive(ToSchema))] diff --git a/skootrs-statestore/src/lib.rs b/skootrs-statestore/src/lib.rs index 0863f26..d357e02 100644 --- a/skootrs-statestore/src/lib.rs +++ b/skootrs-statestore/src/lib.rs @@ -67,11 +67,8 @@ impl ProjectStateStore for GitProjectStateStore { Ok(Some(serde_json::from_str(&project).unwrap())) } - fn update( - &self, - project: InitializedProject, - ) -> impl std::future::Future> + Send { - self.create(project) + async fn update(&self, project: InitializedProject) -> Result<(), SkootError> { + self.create(project).await } } @@ -86,6 +83,10 @@ pub trait ProjectReferenceCache { &mut self, repo_url: String, ) -> impl std::future::Future> + Send; + fn delete( + &mut self, + repo_url: String, + ) -> impl std::future::Future> + Send; } pub struct InMemoryProjectReferenceCache { @@ -103,12 +104,6 @@ impl ProjectReferenceCache for InMemoryProjectReferenceCache { async fn get(&mut self, repo_url: String) -> Result { let repo = InitializedRepo::try_from(repo_url)?; - /*let local_clone = self - .local_repo_service - .clone_local(repo, self.clone_path.clone())?; - let project = - self.local_source_service - .read_file(&local_clone, "./", ".skootrs".to_string())?;*/ let project = self .local_repo_service .fetch_file_content(&repo, ".skootrs") @@ -122,6 +117,12 @@ impl ProjectReferenceCache for InMemoryProjectReferenceCache { self.save()?; Ok(()) } + + async fn delete(&mut self, repo_url: String) -> Result<(), SkootError> { + self.cache.remove(&repo_url); + self.save()?; + Ok(()) + } } impl InMemoryProjectReferenceCache {