diff --git a/coffee_cmd/src/main.rs b/coffee_cmd/src/main.rs index db4ddeca..d27b89f2 100644 --- a/coffee_cmd/src/main.rs +++ b/coffee_cmd/src/main.rs @@ -7,7 +7,7 @@ use radicle_term as term; use coffee_core::coffee::CoffeeManager; use coffee_lib::errors::CoffeeError; use coffee_lib::plugin_manager::PluginManager; -use coffee_lib::types::response::{NurseStatus, UpgradeStatus}; +use coffee_lib::types::response::UpgradeStatus; use crate::cmd::CoffeeArgs; use crate::cmd::CoffeeCommand; @@ -125,19 +125,9 @@ async fn main() -> Result<(), CoffeeError> { }, CoffeeCommand::Nurse {} => match coffee.nurse().await { Ok(val) => { - match val.status { - NurseStatus::Sane => { - term::success!("coffee configuration is not corrupt!") - } - NurseStatus::RepositoryLocallyAbsent => { - term::success!("A repository was locally absent"); - } - NurseStatus::RepositoryLocallyCorrupt => { - term::success!("A repository was locally corrupt"); - } - NurseStatus::RepositoryMissingInConfiguration => { - term::success!("A repository was missing in the configuration"); - } + // For every status in CoffeeNurse, print it + for status in val.status { + term::success!("{}", status.to_string()); } Ok(()) } diff --git a/coffee_core/src/coffee.rs b/coffee_core/src/coffee.rs index 90f46c88..3a30817e 100644 --- a/coffee_core/src/coffee.rs +++ b/coffee_core/src/coffee.rs @@ -1,5 +1,5 @@ //! Coffee mod implementation -use coffee_storage::nosql_db::NoSQlStorage; + use std::collections::HashMap; use std::fmt::Debug; use std::vec::Vec; @@ -23,6 +23,7 @@ use coffee_lib::types::response::*; use coffee_lib::url::URL; use coffee_lib::{commit_id, error, get_repo_info, sh}; use coffee_storage::model::repository::{Kind, Repository as RepositoryInfo}; +use coffee_storage::nosql_db::NoSQlStorage; use coffee_storage::storage::StorageManager; use super::config; @@ -78,6 +79,11 @@ pub struct CoffeeManager { } impl CoffeeManager { + /// return the repos of the plugin manager + pub fn repos(&self) -> &HashMap> { + &self.repos + } + pub async fn new(conf: &dyn CoffeeArgs) -> Result { let conf = CoffeeConf::new(conf).await?; let mut coffee = CoffeeManager { @@ -443,7 +449,69 @@ impl PluginManager for CoffeeManager { } async fn nurse(&mut self) -> Result { - self.recovery_strategies.scan().await + let status = self.recovery_strategies.scan(self).await?; + let mut nurse_actions: Vec = vec![]; + for defect in status.defects.iter() { + log::debug!("defect: {:?}", defect); + match defect { + Defect::RepositoryLocallyAbsent(repos) => { + let mut actions = self.patch_repository_locally_absent(repos.to_vec()).await?; + nurse_actions.append(&mut actions); + } + } + } + + // If there was no actions taken by nurse, we return a sane status. + if nurse_actions.is_empty() { + nurse_actions.push(NurseStatus::Sane); + } + Ok(CoffeeNurse { + status: nurse_actions, + }) + } + + async fn patch_repository_locally_absent( + &mut self, + repos: Vec, + ) -> Result, CoffeeError> { + // initialize the nurse actions + let mut nurse_actions: Vec = vec![]; + // for every repository that is absent locally + // we try to recover it. + // There are 2 cases: + // 1. the repository can be recovered from the remote + // 2. the repository can't be recovered from the remote. In this case + // we remove the repository from the coffee configuration. + for repo_name in repos.iter() { + // Get the repository from the name + let repo = self + .repos + .get_mut(repo_name) + .ok_or_else(|| error!("repository with name: {repo_name} not found"))?; + + match repo.recover().await { + Ok(_) => { + log::info!("repository {} recovered", repo_name.clone()); + nurse_actions.push(NurseStatus::RepositoryLocallyRestored(vec![ + repo_name.clone() + ])); + } + Err(err) => { + log::debug!( + "error while recovering repository {}: {err}", + repo_name.clone() + ); + log::info!("removing repository {}", repo_name.clone()); + self.repos.remove(repo_name); + log::debug!("remote removed: {}", repo_name); + self.flush().await?; + nurse_actions.push(NurseStatus::RepositoryLocallyRemoved(vec![ + repo_name.clone() + ])); + } + } + } + Ok(nurse_actions) } } diff --git a/coffee_core/src/nurse/chain.rs b/coffee_core/src/nurse/chain.rs index 41b1f4d7..921d3820 100644 --- a/coffee_core/src/nurse/chain.rs +++ b/coffee_core/src/nurse/chain.rs @@ -31,16 +31,17 @@ use std::sync::Arc; use async_trait::async_trait; use coffee_lib::errors::CoffeeError; -use coffee_lib::types::response::{CoffeeNurse, NurseStatus}; +use coffee_lib::types::response::{ChainOfResponsibilityStatus, Defect}; use super::strategy::GitRepositoryLocallyAbsentStrategy; -use super::strategy::RecoveryStrategy; +use crate::coffee::CoffeeManager; #[async_trait] pub trait Handler: Send + Sync { async fn can_be_applied( self: Arc, - ) -> Result>, CoffeeError>; + coffee: &CoffeeManager, + ) -> Result, CoffeeError>; } pub struct RecoveryChainOfResponsibility { @@ -48,20 +49,26 @@ pub struct RecoveryChainOfResponsibility { } impl RecoveryChainOfResponsibility { + /// Create a new instance of the chain of responsibility pub async fn new() -> Result { Ok(Self { handlers: vec![Arc::new(GitRepositoryLocallyAbsentStrategy)], }) } - pub async fn scan(&self) -> Result { + /// Scan the chain of responsibility to see what can be applied + /// and return the status of the chain of responsibility + /// with the list of defects + pub async fn scan( + &self, + coffee: &CoffeeManager, + ) -> Result { + let mut defects: Vec = vec![]; for handler in self.handlers.iter() { - if let Some(strategy) = handler.clone().can_be_applied().await? { - return strategy.patch().await; + if let Some(defect) = handler.clone().can_be_applied(coffee).await? { + defects.push(defect); } } - Ok(CoffeeNurse { - status: NurseStatus::Sane, - }) + Ok(ChainOfResponsibilityStatus { defects }) } } diff --git a/coffee_core/src/nurse/strategy.rs b/coffee_core/src/nurse/strategy.rs index c3afb909..7a47bb6f 100644 --- a/coffee_core/src/nurse/strategy.rs +++ b/coffee_core/src/nurse/strategy.rs @@ -21,20 +21,17 @@ //! be able to choose the algorithm at runtime. //! //! Author: Vincenzo Palazzo +use std::path::Path; use std::sync::Arc; use async_trait::async_trait; use coffee_lib::errors::CoffeeError; -use coffee_lib::types::response::{CoffeeNurse, NurseStatus}; +use coffee_lib::types::response::Defect; +use crate::coffee::CoffeeManager; use crate::nurse::chain::Handler; -#[async_trait] -pub trait RecoveryStrategy: Send + Sync { - async fn patch(&self) -> Result; -} - /// Strategy for handling the situation when a Git repository exists in coffee configuration /// but is absent from the local storage. /// @@ -44,29 +41,9 @@ pub trait RecoveryStrategy: Send + Sync { /// a change in the storage location. pub struct GitRepositoryLocallyAbsentStrategy; -#[async_trait] -impl RecoveryStrategy for GitRepositoryLocallyAbsentStrategy { - /// Attempts to address the absence of a Git repository from local storage. - /// - /// This method is responsible for managing the scenario where a Git repository is listed - /// in the coffee configuration but is not present in the `.coffee/repositories` folder. - /// - /// It takes the following actions: - /// - /// 1. Attempts to clone the repository using the Git HEAD reference stored in the configuration. - /// This is done in an effort to retrieve the missing repository from its source. - /// - /// 2. If the cloning process fails, it will remove the repository entry from the coffee configuration. - async fn patch(&self) -> Result { - Ok(CoffeeNurse { - status: NurseStatus::RepositoryLocallyAbsent, - }) - } -} - #[async_trait] impl Handler for GitRepositoryLocallyAbsentStrategy { - /// Determines if [`GitRepositoryLocallyAbsentStrategy`] can be applied. + /// Determines if a repository is missing from local storage. /// /// This function iterates over the Git repositories listed in the coffee configuration and /// checks if each one exists in the `.coffee/repositories` folder. If any repository is found @@ -74,7 +51,26 @@ impl Handler for GitRepositoryLocallyAbsentStrategy { /// this situation should be applied. async fn can_be_applied( self: Arc, - ) -> Result>, CoffeeError> { - Ok(Some(self)) + coffee: &CoffeeManager, + ) -> Result, CoffeeError> { + let mut repos: Vec = Vec::new(); + let coffee_repos = coffee.repos(); + for repo in coffee_repos.values() { + log::debug!("Checking if repository {} exists locally", repo.name()); + let repo_path = repo.url().path_string; + let repo_path = Path::new(&repo_path); + if !repo_path.exists() { + log::debug!("Repository {} is missing locally", repo.name()); + repos.push(repo.name().to_string()); + } + } + + if repos.is_empty() { + log::debug!("No repositories missing locally"); + return Ok(None); + } else { + log::debug!("Found {} repositories missing locally", repos.len()); + return Ok(Some(Defect::RepositoryLocallyAbsent(repos))); + } } } diff --git a/coffee_github/src/repository.rs b/coffee_github/src/repository.rs index d611f5fe..9df62ce1 100644 --- a/coffee_github/src/repository.rs +++ b/coffee_github/src/repository.rs @@ -268,6 +268,46 @@ impl Repository for Github { }) } + async fn recover(&mut self) -> Result<(), CoffeeError> { + let commit = self.git_head.clone(); + + debug!( + "recovering repository: {} {} > {}", + self.name, &self.url.url_string, &self.url.path_string, + ); + // recursively clone the repository + let res = git2::Repository::clone(&self.url.url_string, &self.url.path_string); + match res { + Ok(repo) => { + // get the commit id + let oid = git2::Oid::from_str(&commit.unwrap()) + .map_err(|err| error!("{}", err.message()))?; + // Retrieve the commit associated with the OID + let target_commit = match repo.find_commit(oid) { + Ok(commit) => commit, + Err(err) => return Err(error!("{}", err.message())), + }; + + // Update HEAD to point to the target commit + repo.set_head_detached(target_commit.id()) + .map_err(|err| error!("{}", err.message()))?; + + // retrieve the submodules + let submodules = repo.submodules().unwrap_or_default(); + for (_, sub) in submodules.iter().enumerate() { + let path = + format!("{}/{}", &self.url.path_string, sub.path().to_str().unwrap()); + if let Err(err) = git2::Repository::clone(sub.url().unwrap(), &path) { + return Err(error!("{}", err.message())); + } + } + + Ok(()) + } + Err(err) => Err(error!("{}", err.message())), + } + } + /// list of the plugin installed inside the repository. async fn list(&self) -> Result, CoffeeError> { Ok(self.plugins.clone()) diff --git a/coffee_lib/src/plugin_manager.rs b/coffee_lib/src/plugin_manager.rs index caff9928..e900d69f 100644 --- a/coffee_lib/src/plugin_manager.rs +++ b/coffee_lib/src/plugin_manager.rs @@ -48,4 +48,11 @@ pub trait PluginManager { /// clean up storage information about the remote repositories of the plugin manager. async fn nurse(&mut self) -> Result; + + /// patch coffee configuration in the case that a repository is present in the coffee + /// configuration but is absent from the local storage. + async fn patch_repository_locally_absent( + &mut self, + repos: Vec, + ) -> Result, CoffeeError>; } diff --git a/coffee_lib/src/repository.rs b/coffee_lib/src/repository.rs index 8b45a293..b34a3ee0 100644 --- a/coffee_lib/src/repository.rs +++ b/coffee_lib/src/repository.rs @@ -27,6 +27,9 @@ pub trait Repository: Any { /// upgrade the repository async fn upgrade(&mut self, plugins: &Vec) -> Result; + /// recover the repository from the commit id. + async fn recover(&mut self) -> Result<(), CoffeeError>; + /// return the name of the repository. fn name(&self) -> String; diff --git a/coffee_lib/src/types/mod.rs b/coffee_lib/src/types/mod.rs index 4ff7374c..febfb80c 100644 --- a/coffee_lib/src/types/mod.rs +++ b/coffee_lib/src/types/mod.rs @@ -86,6 +86,7 @@ pub mod request { // Definition of the response types. pub mod response { use serde::{Deserialize, Serialize}; + use std::fmt; use crate::plugin::Plugin; @@ -140,19 +141,50 @@ pub mod response { pub plugin: Plugin, } + /// This struct is used to represent a defect + /// that can be patched by the nurse. + #[derive(Clone, Debug, Serialize, Deserialize)] + pub enum Defect { + // A patch operation when a git repository is present in the coffee configuration + // but is absent from the local storage. + RepositoryLocallyAbsent(Vec), + // TODO: Add more patch operations + } + + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct ChainOfResponsibilityStatus { + pub defects: Vec, + } + /// This struct is used to represent the status of nurse, /// either sane or not. - /// If not sane, return the reason of the problem. + /// If not sane, return the action that nurse has taken. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum NurseStatus { Sane, - RepositoryLocallyAbsent, - RepositoryLocallyCorrupt, - RepositoryMissingInConfiguration, + RepositoryLocallyRestored(Vec), + RepositoryLocallyRemoved(Vec), } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CoffeeNurse { - pub status: NurseStatus, + pub status: Vec, + } + + impl fmt::Display for NurseStatus { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + NurseStatus::Sane => write!( + f, + "coffee configuration is not corrupt! No need to run coffee nurse" + ), + NurseStatus::RepositoryLocallyRestored(val) => { + write!(f, "Repositories restored locally: {}", val.join(" ")) + } + NurseStatus::RepositoryLocallyRemoved(val) => { + write!(f, "Repositories removed locally: {}", val.join(" ")) + } + } + } } }