From 8911d8f6bc2689f36a407e597eab2c477294565c Mon Sep 17 00:00:00 2001
From: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
Date: Tue, 9 Apr 2024 14:26:37 +0200
Subject: [PATCH] core: fixing the nurse run command

Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
---
 coffee_cmd/src/coffee_term/command_show.rs | 51 +++++++++++++++++++++-
 coffee_cmd/src/main.rs                     |  4 +-
 coffee_core/src/coffee.rs                  | 21 ++++-----
 coffee_core/src/config.rs                  | 35 ++++++++-------
 coffee_core/src/nurse/chain.rs             |  2 +-
 coffee_core/src/nurse/strategy.rs          |  5 +++
 coffee_github/src/repository.rs            |  4 ++
 coffee_lib/src/repository.rs               |  5 +++
 coffee_lib/src/types/mod.rs                | 35 ++-------------
 coffee_lib/src/url.rs                      |  4 ++
 coffee_lib/src/utils.rs                    | 35 ++++++++++++++-
 rust-toolchain                             |  2 +-
 12 files changed, 136 insertions(+), 67 deletions(-)

diff --git a/coffee_cmd/src/coffee_term/command_show.rs b/coffee_cmd/src/coffee_term/command_show.rs
index 0977600..28497d8 100644
--- a/coffee_cmd/src/coffee_term/command_show.rs
+++ b/coffee_cmd/src/coffee_term/command_show.rs
@@ -7,7 +7,10 @@ use term::Element;
 
 use coffee_lib::error;
 use coffee_lib::errors::CoffeeError;
-use coffee_lib::types::response::{CoffeeList, CoffeeNurse, CoffeeRemote, CoffeeTip, NurseStatus};
+use coffee_lib::types::response::{
+    ChainOfResponsibilityStatus, CoffeeList, CoffeeNurse, CoffeeRemote, CoffeeTip, Defect,
+    NurseStatus,
+};
 
 pub fn show_list(coffee_list: Result<CoffeeList, CoffeeError>) -> Result<(), CoffeeError> {
     let remotes = coffee_list?;
@@ -86,6 +89,52 @@ pub fn show_remote_list(remote_list: Result<CoffeeRemote, CoffeeError>) -> Resul
     Ok(())
 }
 
+pub fn show_nurse_verify(nurse_verify: &ChainOfResponsibilityStatus) -> Result<(), CoffeeError> {
+    if nurse_verify.defects.is_empty() {
+        term::success!("Coffee configuration is not corrupt! No need to run coffee nurse");
+    } else {
+        let mut table = radicle_term::Table::new(TableOptions::bordered());
+        table.push([
+            term::format::dim(String::from("●")),
+            term::format::bold(String::from("Defects")),
+            term::format::bold(String::from("Affected repositories")),
+        ]);
+        table.divider();
+
+        for defect in nurse_verify.defects.iter() {
+            match defect {
+                Defect::RepositoryLocallyAbsent(repos) => {
+                    let defect = "Repository missing locally";
+                    for repo in repos {
+                        table.push([
+                            term::format::positive("●").into(),
+                            term::format::bold(defect.to_owned()),
+                            term::format::highlight(repo.clone()),
+                        ]);
+                    }
+                }
+                Defect::CoffeeGlobalrepoCleanup(networks) => {
+                    let defect = format!(
+                        "Global repository migration completed for the networks: {}",
+                        networks
+                            .iter()
+                            .map(|(network, _)| format!("{network} "))
+                            .collect::<String>()
+                            .trim_end()
+                    );
+                    table.push([
+                        term::format::positive("●").into(),
+                        term::format::bold(defect.to_owned()),
+                        term::format::highlight("_".to_owned()),
+                    ]);
+                }
+            }
+        }
+        table.print();
+    }
+    Ok(())
+}
+
 pub fn show_nurse_result(
     nurse_result: Result<CoffeeNurse, CoffeeError>,
 ) -> Result<(), CoffeeError> {
diff --git a/coffee_cmd/src/main.rs b/coffee_cmd/src/main.rs
index c64106e..5cd7a4f 100644
--- a/coffee_cmd/src/main.rs
+++ b/coffee_cmd/src/main.rs
@@ -154,9 +154,9 @@ async fn run(args: CoffeeArgs, mut coffee: CoffeeManager) -> Result<(), CoffeeEr
         CoffeeCommand::Nurse { verify } => {
             if verify {
                 let result = coffee.nurse_verify().await?;
-                term::info!("{}", result);
+                coffee_term::show_nurse_verify(&result)?;
                 if !result.is_sane() {
-                    term::info!("Coffee local directory is damaged, please run `coffee nurse` to try to fix it");
+                    term::warning(term::style("Coffee local directory is damaged, please run `coffee nurse` to try to fix it").bold());
                 }
             } else {
                 let nurse_result = coffee.nurse().await;
diff --git a/coffee_core/src/coffee.rs b/coffee_core/src/coffee.rs
index 79a3a50..0ed4adf 100644
--- a/coffee_core/src/coffee.rs
+++ b/coffee_core/src/coffee.rs
@@ -21,7 +21,7 @@ use coffee_lib::plugin_manager::PluginManager;
 use coffee_lib::repository::Repository;
 use coffee_lib::types::response::*;
 use coffee_lib::url::URL;
-use coffee_lib::utils::{copy_dir_if_exist, rm_dir_if_exist};
+use coffee_lib::utils::{check_dir_or_make_if_missing, copy_dir_if_exist, rm_dir_if_exist};
 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;
@@ -80,8 +80,8 @@ pub struct CoffeeManager {
 }
 
 impl CoffeeManager {
-    pub async fn new(conf: &dyn CoffeeArgs) -> Result<Self, CoffeeError> {
-        let conf = CoffeeConf::new(conf).await?;
+    pub async fn new(conf_args: &dyn CoffeeArgs) -> Result<Self, CoffeeError> {
+        let conf = CoffeeConf::new(conf_args).await?;
         let mut coffee = CoffeeManager {
             config: conf.clone(),
             coffee_cln_config: CLNConf::new(conf.config_path, true),
@@ -98,6 +98,7 @@ impl CoffeeManager {
     /// when coffee is configured, run an inventory to collect all the necessary information
     /// about the coffee ecosystem.
     async fn inventory(&mut self) -> Result<(), CoffeeError> {
+        let skip_verify = self.config.skip_verify;
         let _ = self
             .storage
             .load::<CoffeeStorageInfo>(&self.config.network)
@@ -123,7 +124,7 @@ impl CoffeeManager {
         if let Err(err) = self.coffee_cln_config.parse() {
             log::error!("{}", err.cause);
         }
-        if !self.config.skip_verify {
+        if !skip_verify {
             // Check for the chain of responsibility
             let status = self.recovery_strategies.scan(self).await?;
             log::debug!("Chain of responsibility status: {:?}", status);
@@ -427,15 +428,10 @@ impl PluginManager for CoffeeManager {
     }
 
     async fn add_remote(&mut self, name: &str, url: &str) -> Result<(), CoffeeError> {
-        // FIXME: we should allow some error here like
-        // for the add remote command the no found error for the `repository`
-        // directory is fine.
-
         if self.repos.contains_key(name) {
             return Err(error!("repository with name: {name} already exists"));
         }
-        let local_path = format!("{}/{}", self.config.root_path, self.config.network);
-        let url = URL::new(&local_path, url, name);
+        let url = URL::new(&self.config.path(), url, name);
         log::debug!("remote adding: {} {}", name, &url.url_string);
         let mut repo = Github::new(name, &url);
         repo.init().await?;
@@ -551,6 +547,8 @@ impl PluginManager for CoffeeManager {
                 Defect::CoffeeGlobalrepoCleanup(networks) => {
                     let global_repo = format!("{}/repositories", self.config.root_path);
                     for (network, path) in networks {
+                        log::info!("{network} - {path}");
+                        check_dir_or_make_if_missing(path.to_owned()).await?;
                         if !Path::exists(Path::new(&path)) {
                             copy_dir_if_exist(&global_repo, path).await?;
                         }
@@ -565,6 +563,8 @@ impl PluginManager for CoffeeManager {
             status: nurse_actions,
         };
         nurse.organize();
+        // Refesh the status
+        self.flush().await?;
         Ok(nurse)
     }
 
@@ -587,6 +587,7 @@ impl PluginManager for CoffeeManager {
                 .get_mut(repo_name)
                 .ok_or_else(|| error!("repository with name: {repo_name} not found"))?;
 
+            repo.change_root_path(&self.config.path());
             match repo.recover().await {
                 Ok(_) => {
                     log::info!("repository {} recovered", repo_name.clone());
diff --git a/coffee_core/src/config.rs b/coffee_core/src/config.rs
index 5f9e287..1f5328d 100644
--- a/coffee_core/src/config.rs
+++ b/coffee_core/src/config.rs
@@ -1,13 +1,14 @@
 //! Coffee configuration utils.
-use log::info;
-use serde::{Deserialize, Serialize};
 use std::env;
 
-use crate::CoffeeOperation;
-use coffee_lib::utils::{check_dir_or_make_if_missing, copy_dir_if_exist};
+use serde::{Deserialize, Serialize};
+
+use coffee_lib::utils::check_dir_or_make_if_missing;
 use coffee_lib::{errors::CoffeeError, plugin::Plugin};
 
 use crate::CoffeeArgs;
+use crate::CoffeeOperation;
+
 /// Custom coffee configuration, given by a command line list of arguments
 /// or a coffee configuration file.
 #[derive(Clone, Debug, Serialize, Deserialize)]
@@ -15,23 +16,25 @@ pub struct CoffeeConf {
     /// Network configuration related
     /// to core lightning network
     pub network: String,
+    /// root path plugin manager
+    pub root_path: String,
     /// path of core lightning configuration file
     /// managed by coffee
     pub config_path: String,
     /// path of the core lightning configuration file
     /// not managed by core lightning
-    /// (this file included the file managed by coffee)
+    ///
+    /// This file included the file managed by coffee
     pub cln_config_path: Option<String>,
     /// root cln directory path
     pub cln_root: Option<String>,
-    /// root path plugin manager
-    pub root_path: String,
     /// all plugins that are installed
     /// with the plugin manager.
     pub plugins: Vec<Plugin>,
     /// A flag that indicates if the
     /// user wants to skip the verification
     /// of nurse.
+    #[serde(skip)]
     pub skip_verify: bool,
 }
 
@@ -47,7 +50,7 @@ impl CoffeeConf {
         def_path = def_path.strip_suffix('/').unwrap_or(&def_path).to_string();
         def_path += "/.coffee";
         check_dir_or_make_if_missing(def_path.to_string()).await?;
-        info!("creating coffee home at {def_path}");
+        log::info!("creating coffee home at {def_path}");
 
         let mut coffee = CoffeeConf {
             network: "bitcoin".to_owned(),
@@ -62,14 +65,8 @@ impl CoffeeConf {
         // check the command line arguments and bind them
         // inside the coffee conf
         coffee.bind_cmd_line_params(conf)?;
-
         check_dir_or_make_if_missing(format!("{def_path}/{}", coffee.network)).await?;
         check_dir_or_make_if_missing(format!("{def_path}/{}/plugins", coffee.network)).await?;
-        let repo_dir = format!("{def_path}/{}/repositories", coffee.network);
-        // older version of coffee has a repository inside the directory
-        copy_dir_if_exist(&format!("{def_path}/repositories"), &repo_dir).await?;
-        // FIXME: nurse should clean up the  `{def_path}/repositories`.
-        check_dir_or_make_if_missing(repo_dir).await?;
         // after we know all the information regarding
         // the configuration we try to see if there is
         // something stored already to the disk.
@@ -105,10 +102,12 @@ impl CoffeeConf {
                 }
             }
         }
-
-        // FIXME: be able to put the directory also in another place!
-        // for now it is fixed in the Home/.coffee but another good place
-        // will be, the .lightning dir
         Ok(())
     }
+
+    /// Return the root path of the coffee manager instance
+    /// this include also the network path.
+    pub fn path(&self) -> String {
+        format!("{}/{}", self.root_path, self.network)
+    }
 }
diff --git a/coffee_core/src/nurse/chain.rs b/coffee_core/src/nurse/chain.rs
index 2443772..a189271 100644
--- a/coffee_core/src/nurse/chain.rs
+++ b/coffee_core/src/nurse/chain.rs
@@ -53,8 +53,8 @@ impl RecoveryChainOfResponsibility {
     pub async fn new() -> Result<Self, CoffeeError> {
         Ok(Self {
             handlers: vec![
-                Arc::new(GitRepositoryLocallyAbsentStrategy),
                 Arc::new(CoffeeRepositoryDirCleanUp),
+                Arc::new(GitRepositoryLocallyAbsentStrategy),
             ],
         })
     }
diff --git a/coffee_core/src/nurse/strategy.rs b/coffee_core/src/nurse/strategy.rs
index fca65fe..0ecfc55 100644
--- a/coffee_core/src/nurse/strategy.rs
+++ b/coffee_core/src/nurse/strategy.rs
@@ -94,6 +94,11 @@ impl Handler for CoffeeRepositoryDirCleanUp {
         // Check whether there exists a network-specific repositories folder for each network.
         let mut directory_moving = vec![];
         for network in networks {
+            let network_dir = format!("{}/{network}", coffee.config.root_path);
+            if !Path::exists(Path::new(&network_dir)) {
+                log::debug!("network dir `{network_dir}` not found");
+                continue;
+            }
             let subpath_repo = format!("{}/{network}/repositories", coffee.config.root_path);
             if !Path::exists(Path::new(&subpath_repo)) {
                 directory_moving.push((network.to_string(), subpath_repo));
diff --git a/coffee_github/src/repository.rs b/coffee_github/src/repository.rs
index be237e6..fa6f7e2 100644
--- a/coffee_github/src/repository.rs
+++ b/coffee_github/src/repository.rs
@@ -233,6 +233,10 @@ impl Repository for Github {
         }
     }
 
+    fn change_root_path(&mut self, path: &str) {
+        self.url.set_coffee_path(path);
+    }
+
     async fn upgrade(
         &mut self,
         plugins: &Vec<Plugin>,
diff --git a/coffee_lib/src/repository.rs b/coffee_lib/src/repository.rs
index dbb9bec..586a51d 100644
--- a/coffee_lib/src/repository.rs
+++ b/coffee_lib/src/repository.rs
@@ -34,6 +34,11 @@ pub trait Repository: Any {
     /// recover the repository from the commit id.
     async fn recover(&mut self) -> Result<(), CoffeeError>;
 
+    /// While migrating there is a possibility that we should
+    /// move an old repository into a new path. So this
+    /// is semplyfing this process.
+    fn change_root_path(&mut self, path: &str);
+
     /// 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 0fb1e21..ba33b74 100644
--- a/coffee_lib/src/types/mod.rs
+++ b/coffee_lib/src/types/mod.rs
@@ -166,37 +166,6 @@ pub mod response {
         }
     }
 
-    impl fmt::Display for ChainOfResponsibilityStatus {
-        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-            if self.defects.is_empty() {
-                write!(f, "Coffee is sane")
-            } else {
-                writeln!(f, "Coffee has the following defects:")?;
-                for (i, defect) in self.defects.iter().enumerate() {
-                    match defect {
-                        Defect::RepositoryLocallyAbsent(repos) => {
-                            write!(f, "{}. Repository missing locally: ", i + 1)?;
-                            for repo in repos {
-                                write!(f, " {}", repo)?;
-                            }
-                        }
-                        Defect::CoffeeGlobalrepoCleanup(networks) => {
-                            writeln!(
-                                f,
-                                "Global repository migration completed for the networks: {}",
-                                networks
-                                    .iter()
-                                    .map(|(network, _)| network.to_owned())
-                                    .collect::<String>()
-                            )?;
-                        }
-                    }
-                }
-                Ok(())
-            }
-        }
-    }
-
     /// This struct is used to represent the status of nurse,
     /// either sane or not.
     /// If not sane, return the action that nurse has taken.
@@ -231,7 +200,9 @@ pub mod response {
                     NurseStatus::RepositoryLocallyRestored(repos) => {
                         repositories_locally_restored.append(&mut repos.clone())
                     }
-                    NurseStatus::MovingGlobalRepostoryTo(_) => {}
+                    NurseStatus::MovingGlobalRepostoryTo(status) => {
+                        new_status.push(NurseStatus::MovingGlobalRepostoryTo(status.to_owned()));
+                    }
                 }
             }
             if !repositories_locally_removed.is_empty() {
diff --git a/coffee_lib/src/url.rs b/coffee_lib/src/url.rs
index c7dc4b8..9b4dcd7 100644
--- a/coffee_lib/src/url.rs
+++ b/coffee_lib/src/url.rs
@@ -57,6 +57,10 @@ impl URL {
             repo_name: get_repo_name_from_url(url),
         }
     }
+
+    pub fn set_coffee_path(&mut self, path: &str) {
+        self.path_string = format!("{path}/{}", self.repo_name);
+    }
 }
 
 impl fmt::Display for URL {
diff --git a/coffee_lib/src/utils.rs b/coffee_lib/src/utils.rs
index f951aae..ca7717c 100644
--- a/coffee_lib/src/utils.rs
+++ b/coffee_lib/src/utils.rs
@@ -1,5 +1,5 @@
 use super::macros::error;
-use std::path::Path;
+use std::path::{Path, PathBuf};
 
 use tokio::fs;
 
@@ -21,6 +21,7 @@ pub fn get_plugin_info_from_path(path: &Path) -> Result<(String, String), Coffee
 }
 
 pub async fn check_dir_or_make_if_missing(path: String) -> Result<(), CoffeeError> {
+    log::trace!("check_dir_or_make_if_missing: `{path}`");
     if !Path::exists(Path::new(&path.to_owned())) {
         fs::create_dir(path.clone()).await?;
         log::debug!("created dir {path}");
@@ -29,13 +30,43 @@ pub async fn check_dir_or_make_if_missing(path: String) -> Result<(), CoffeeErro
 }
 
 pub async fn copy_dir_if_exist(origin: &str, destination: &str) -> Result<(), CoffeeError> {
+    log::trace!("copy_dir_if_exist: from: `{origin}` to `{destination}`");
     if Path::exists(Path::new(&origin)) {
-        fs::copy(origin, destination).await?;
+        copy_dir_recursive(origin.to_owned(), destination.to_owned()).await?;
         log::debug!("copying dir from {origin} to {destination}");
     }
     Ok(())
 }
 
+async fn copy_dir_recursive(source: String, destination: String) -> Result<(), CoffeeError> {
+    async fn inner_copy_dir_recursive(
+        source: PathBuf,
+        destination: PathBuf,
+    ) -> Result<(), CoffeeError> {
+        check_dir_or_make_if_missing(destination.to_string_lossy().to_string()).await?;
+
+        let mut entries = fs::read_dir(source).await?;
+        while let Some(entry) = entries.next_entry().await? {
+            let file_type = entry.file_type().await?;
+            let dest_path = destination.join(entry.file_name());
+            log::debug!("copy entry {:?} in {:?}", entry, dest_path);
+            if file_type.is_dir() {
+                // Here we use Box::pin to allow recursion
+                let fut = inner_copy_dir_recursive(entry.path(), dest_path);
+                Box::pin(fut).await?;
+            } else if file_type.is_file() {
+                fs::copy(entry.path(), &dest_path).await?;
+            }
+        }
+
+        Ok(())
+    }
+    let source = Path::new(&source);
+    let destination = Path::new(&destination);
+    log::info!("{:?} - {:?}", source, destination);
+    inner_copy_dir_recursive(source.to_path_buf(), destination.to_path_buf()).await
+}
+
 pub async fn rm_dir_if_exist(origin: &str) -> Result<(), CoffeeError> {
     if Path::exists(Path::new(&origin)) {
         fs::remove_dir_all(origin).await?;
diff --git a/rust-toolchain b/rust-toolchain
index 07cde98..3245dca 100644
--- a/rust-toolchain
+++ b/rust-toolchain
@@ -1 +1 @@
-1.75
+1.77