Repository: comtrya/comtrya (language: rust)

Issue:
New Feature: Dry Run
Allow for a `—dry-run` flag to be provided which will show which manifests will be executed and which packages and files each will provision.


Discussion / hints:

Fix patch:
diff --git a/src/actions/command/run.rs b/src/actions/command/run.rs
index 7f93b2c4..03103612 100644
--- a/src/actions/command/run.rs
+++ b/src/actions/command/run.rs
@@ -21,19 +21,35 @@ fn get_false() -> bool {
 }
 
 impl Action for CommandRun {
+    fn dry_run(
+        &self,
+        _manifest: &crate::manifests::Manifest,
+        _context: &tera::Context,
+    ) -> Result<ActionResult, ActionError> {
+        let pretty_args = self
+            .args
+            .iter()
+            .map(|a| format!("\"{}\"", a))
+            .collect::<Vec<_>>()
+            .join(", ");
+        Ok(ActionResult {
+            message: format!(
+                "run {} with args {} (require_root={})",
+                self.command, pretty_args, self.sudo
+            ),
+        })
+    }
     fn run(
         &self,
         _manifest: &crate::manifests::Manifest,
         _context: &tera::Context,
     ) -> Result<ActionResult, ActionError> {
-        Ok(crate::utils::command::run_command(
-            crate::utils::command::Command {
-                name: self.command.clone(),
-                env: HashMap::new(),
-                args: self.args.clone(),
-                dir: Some(self.dir.clone()),
-                require_root: self.sudo,
-            },
-        ))?
+        crate::utils::command::run_command(crate::utils::command::Command {
+            name: self.command.clone(),
+            env: HashMap::new(),
+            args: self.args.clone(),
+            dir: Some(self.dir.clone()),
+            require_root: self.sudo,
+        })
     }
 }
diff --git a/src/actions/directory/copy.rs b/src/actions/directory/copy.rs
index b0b0b0c0..034f2c3e 100644
--- a/src/actions/directory/copy.rs
+++ b/src/actions/directory/copy.rs
@@ -1,7 +1,7 @@
 use std::fs::create_dir_all;
 
 use super::DirectoryAction;
-use crate::actions::{Action, ActionError, ActionResult};
+use crate::actions::{Action, ActionError, ActionResult, ActionResultExt};
 use crate::manifests::Manifest;
 use fs_extra::dir::CopyOptions;
 use serde::{Deserialize, Serialize};
@@ -18,6 +18,16 @@ impl DirectoryCopy {}
 impl DirectoryAction for DirectoryCopy {}
 
 impl Action for DirectoryCopy {
+    fn dry_run(
+        &self,
+        _manifest: &Manifest,
+        _context: &Context,
+    ) -> Result<ActionResult, ActionError> {
+        Ok(ActionResult {
+            message: format!("copy directory from {} to {}", self.from, self.to),
+        })
+    }
+
     fn run(&self, manifest: &Manifest, _context: &Context) -> Result<ActionResult, ActionError> {
         let absolute_path = manifest
             .root_dir
@@ -26,16 +36,9 @@ impl Action for DirectoryCopy {
             .join("files")
             .join(&self.from);
 
-        match create_dir_all(&self.to) {
-            Ok(_) => (),
-            Err(_) => {
-                return Err(ActionError {
-                    message: String::from("Failed to create directory"),
-                });
-            }
-        }
+        create_dir_all(&self.to).context("Failed to create directory")?;
 
-        match fs_extra::dir::copy(
+        fs_extra::dir::copy(
             &absolute_path,
             &self.to,
             &CopyOptions {
@@ -43,14 +46,12 @@ impl Action for DirectoryCopy {
                 content_only: true,
                 ..Default::default()
             },
-        ) {
-            Ok(_) => Ok(ActionResult {
-                message: String::from("Copied"),
-            }),
-            Err(e) => Err(ActionError {
-                message: e.to_string(),
-            }),
-        }
+        )
+        .context("Failed to copy directory")?;
+
+        Ok(ActionResult {
+            message: String::from("Copied"),
+        })
     }
 }
 
@@ -58,9 +59,8 @@ impl Action for DirectoryCopy {
 mod tests {
     use std::path::PathBuf;
 
-    use crate::actions::Actions;
+    use crate::actions::{Action, Actions};
     use crate::manifests::Manifest;
-    use crate::Action;
 
     fn get_manifest_dir() -> PathBuf {
         std::env::current_dir()
diff --git a/src/actions/directory/mod.rs b/src/actions/directory/mod.rs
index 926228df..3143b712 100644
--- a/src/actions/directory/mod.rs
+++ b/src/actions/directory/mod.rs
@@ -26,8 +26,6 @@ pub trait DirectoryAction: Action {
 
     fn load(&self, manifest: &Manifest, path: &str) -> Result<String, ActionError> {
         std::fs::read_to_string(manifest.root_dir.clone().unwrap().join("files").join(path))
-            .map_err(|e| ActionError {
-                message: e.to_string(),
-            })
+            .map_err(ActionError::from)
     }
 }
diff --git a/src/actions/file/copy.rs b/src/actions/file/copy.rs
index 192f694b..a1891426 100644
--- a/src/actions/file/copy.rs
+++ b/src/actions/file/copy.rs
@@ -1,5 +1,5 @@
 use super::FileAction;
-use crate::actions::{Action, ActionError, ActionResult};
+use crate::actions::{Action, ActionError, ActionResult, ActionResultExt};
 use crate::manifests::Manifest;
 use serde::{de::Error, Deserialize, Deserializer, Serialize};
 use std::{fs::create_dir_all, ops::Deref, path::PathBuf, u32};
@@ -40,22 +40,25 @@ impl FileCopy {}
 impl FileAction for FileCopy {}
 
 impl Action for FileCopy {
+    fn dry_run(
+        &self,
+        _manifest: &Manifest,
+        _context: &Context,
+    ) -> Result<ActionResult, ActionError> {
+        Ok(ActionResult {
+            message: format!("copy from {} to {}", self.from, self.to),
+        })
+    }
+
     fn run(&self, manifest: &Manifest, context: &Context) -> Result<ActionResult, ActionError> {
         let tera = self.init(manifest);
 
-        let contents = match if self.template {
+        let contents = if self.template {
             tera.render(self.from.clone().deref(), context)
-                .map_err(|e| ActionError {
-                    message: e.to_string(),
-                })
+                .map_err(ActionError::from)
         } else {
             self.load(manifest, &self.from)
-        } {
-            Ok(contents) => contents,
-            Err(error) => {
-                return Err(error);
-            }
-        };
+        }?;
 
         let mut parent = PathBuf::from(&self.to);
         parent.pop();
@@ -65,51 +68,17 @@ impl Action for FileCopy {
             directories = &parent.to_str().unwrap()
         );
 
-        match create_dir_all(parent) {
-            Ok(_) => (),
-            Err(_) => {
-                return Err(ActionError {
-                    message: String::from("Failed to create parent directory"),
-                });
-            }
-        }
-
-        let mut file = match std::fs::File::create(self.to.clone()) {
-            Ok(f) => f,
-            Err(_) => {
-                return Err(ActionError {
-                    message: String::from("Failed to create file"),
-                });
-            }
-        };
+        create_dir_all(parent).context("Failed to create parent directory")?;
 
-        match file.write_all(contents.as_bytes()) {
-            Ok(_) => {}
-            Err(_) => {
-                return Err(ActionError {
-                    message: String::from("Failed to create file"),
-                });
-            }
-        };
+        let mut file = std::fs::File::create(self.to.clone()).context("Failed to create file")?;
 
-        match file.sync_all() {
-            Ok(_) => {}
-            Err(_) => {
-                return Err(ActionError {
-                    message: String::from("Failed to create file"),
-                });
-            }
-        }
-
-        match set_permissions(PathBuf::from(self.to.clone()), self.chmod) {
-            Ok(_) => {}
-            Err(e) => {
-                return Err(ActionError {
-                    message: format!("Failed to set permissions: {}", e.to_string()),
-                })
-            }
-        }
+        file.write_all(contents.as_bytes())
+            .context("Failed to create file")?;
+
+        file.sync_all().context("Failed to create file")?;
 
+        set_permissions(PathBuf::from(self.to.clone()), self.chmod)
+            .context("Failed to set permissions")?;
         Ok(ActionResult {
             message: String::from("Copied"),
         })
diff --git a/src/actions/file/link.rs b/src/actions/file/link.rs
index ffe54257..86350fc8 100644
--- a/src/actions/file/link.rs
+++ b/src/actions/file/link.rs
@@ -1,5 +1,5 @@
 use super::FileAction;
-use crate::actions::{Action, ActionError, ActionResult};
+use crate::actions::{Action, ActionError, ActionResult, ActionResultExt};
 use crate::manifests::Manifest;
 use serde::{Deserialize, Serialize};
 use std::fs::create_dir_all;
@@ -18,6 +18,22 @@ impl FileLink {}
 impl FileAction for FileLink {}
 
 impl Action for FileLink {
+    fn dry_run(
+        &self,
+        manifest: &Manifest,
+        _context: &Context,
+    ) -> Result<ActionResult, ActionError> {
+        let from = self.resolve(manifest, &self.from)?;
+        let to = PathBuf::from(&self.to);
+
+        Ok(ActionResult {
+            message: format!(
+                "symlink from {} to {}",
+                from.to_string_lossy(),
+                to.to_string_lossy()
+            ),
+        })
+    }
     fn run(&self, manifest: &Manifest, _context: &Context) -> Result<ActionResult, ActionError> {
         let mut parent = PathBuf::from(&self.to);
         parent.pop();
@@ -27,16 +43,9 @@ impl Action for FileLink {
             directories = &parent.to_str().unwrap()
         );
 
-        match create_dir_all(parent) {
-            Ok(_) => (),
-            Err(_) => {
-                return Err(ActionError {
-                    message: String::from("Failed to create parent directory"),
-                });
-            }
-        }
+        create_dir_all(parent).context("Failed to create parent directory")?;
 
-        let from = self.resolve(manifest, &self.from).unwrap();
+        let from = self.resolve(manifest, &self.from)?;
         let to = PathBuf::from(&self.to);
 
         match to.read_link() {
@@ -59,36 +68,27 @@ impl Action for FileLink {
 #[cfg(windows)]
 fn create_link(from: PathBuf, to: PathBuf) -> Result<ActionResult, ActionError> {
     if from.is_dir() {
-        match std::os::windows::fs::symlink_dir(from, to).map_err(|e| ActionError {
-            message: e.to_string(),
-        }) {
-            Ok(_) => Ok(ActionResult {
+        std::os::windows::fs::symlink_dir(from, to)
+            .context(format!("A: {:?} - {:?}", from, to))
+            .map(|_| ActionResult {
                 message: String::from("Symlink created"),
-            }),
-            Err(e) => Err(e),
-        }
+            })
     } else {
-        match std::os::windows::fs::symlink_file(from, to).map_err(|e| ActionError {
-            message: e.to_string(),
-        }) {
-            Ok(_) => Ok(ActionResult {
+        std::os::windows::fs::symlink_file(from, to)
+            .context(format!("A: {:?} - {:?}", from, to))
+            .map(|_| ActionResult {
                 message: String::from("Symlink created"),
-            }),
-            Err(e) => Err(e),
-        }
+            })
     }
 }
 
 #[cfg(unix)]
 fn create_link(from: PathBuf, to: PathBuf) -> Result<ActionResult, ActionError> {
-    match std::os::unix::fs::symlink(from.clone(), to.clone()).map_err(|e| ActionError {
-        message: format!("A: {:?} - {:?} - {}", from, to, e.to_string()),
-    }) {
-        Ok(_) => Ok(ActionResult {
+    std::os::unix::fs::symlink(from.clone(), to.clone())
+        .context(format!("A: {:?} - {:?}", from, to))
+        .map(|_| ActionResult {
             message: String::from("Symlink created"),
-        }),
-        Err(e) => Err(e),
-    }
+        })
 }
 
 #[cfg(test)]
diff --git a/src/actions/file/mod.rs b/src/actions/file/mod.rs
index 02d0e3a6..06ff50f1 100644
--- a/src/actions/file/mod.rs
+++ b/src/actions/file/mod.rs
@@ -29,22 +29,33 @@ pub trait FileAction: Action {
     }
 
     fn resolve(&self, manifest: &Manifest, path: &str) -> Result<PathBuf, ActionError> {
-        manifest
-            .root_dir
-            .clone()
-            .unwrap()
-            .join("files")
-            .join(path)
-            .canonicalize()
-            .map_err(|e| ActionError {
-                message: format!("Failed because  {}", e.to_string()),
-            })
+        use std::io::ErrorKind;
+
+        let file_path = manifest.root_dir.clone().unwrap().join("files").join(path);
+
+        file_path.canonicalize().map_err(|e| ActionError {
+            message: match e.kind() {
+                ErrorKind::NotFound => format!(
+                    "Failed because {} was not found",
+                    file_path.to_string_lossy()
+                ),
+                _ => format!("Failed because {}", e.to_string()),
+            },
+        })
     }
 
     fn load(&self, manifest: &Manifest, path: &str) -> Result<String, ActionError> {
-        std::fs::read_to_string(manifest.root_dir.clone().unwrap().join("files").join(path))
-            .map_err(|e| ActionError {
-                message: e.to_string(),
-            })
+        use std::io::ErrorKind;
+        let file_path = manifest.root_dir.clone().unwrap().join("files").join(path);
+
+        std::fs::read_to_string(file_path.clone()).map_err(|e| ActionError {
+            message: match e.kind() {
+                ErrorKind::NotFound => format!(
+                    "Failed because {} was not found",
+                    file_path.to_string_lossy()
+                ),
+                _ => format!("Failed because {}", e.to_string()),
+            },
+        })
     }
 }
diff --git a/src/actions/mod.rs b/src/actions/mod.rs
index e0e30970..d099dbf0 100644
--- a/src/actions/mod.rs
+++ b/src/actions/mod.rs
@@ -27,6 +27,18 @@ pub enum Actions {
     PackageInstall(PackageInstall),
 }
 
+impl Actions {
+    pub fn inner_ref(&self) -> &dyn Action {
+        match self {
+            Actions::CommandRun(a) => a,
+            Actions::DirectoryCopy(a) => a,
+            Actions::FileCopy(a) => a,
+            Actions::FileLink(a) => a,
+            Actions::PackageInstall(a) => a,
+        }
+    }
+}
+
 #[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct ActionResult {
     /// Output / response
@@ -39,6 +51,28 @@ pub struct ActionError {
     pub message: String,
 }
 
+impl<E: std::error::Error> From<E> for ActionError {
+    fn from(e: E) -> Self {
+        ActionError {
+            message: format!("{}", e),
+        }
+    }
+}
+
+pub trait ActionResultExt<M, T> {
+    fn context(self, message: M) -> Result<T, ActionError>;
+}
+
+impl<M: std::fmt::Display, T, E: std::error::Error> ActionResultExt<M, T> for Result<T, E> {
+    fn context(self, message: M) -> Result<T, ActionError> {
+        self.map_err(|e| ActionError {
+            message: format!("{} because of {}", message, e.to_string()),
+        })
+    }
+}
+
 pub trait Action {
     fn run(&self, manifest: &Manifest, context: &Context) -> Result<ActionResult, ActionError>;
+
+    fn dry_run(&self, manifest: &Manifest, context: &Context) -> Result<ActionResult, ActionError>;
 }
diff --git a/src/actions/package/install.rs b/src/actions/package/install.rs
index 7deb1d3d..ab66370d 100644
--- a/src/actions/package/install.rs
+++ b/src/actions/package/install.rs
@@ -9,6 +9,22 @@ use tracing::span;
 pub type PackageInstall = Package;
 
 impl Action for PackageInstall {
+    fn dry_run(
+        &self,
+        _manifest: &Manifest,
+        _context: &Context,
+    ) -> Result<ActionResult, ActionError> {
+        let variant: PackageVariant = self.into();
+
+        Ok(ActionResult {
+            message: format!(
+                "Install {} from {}",
+                variant.packages().join(", "),
+                variant.provider.name()
+            ),
+        })
+    }
+
     fn run(&self, _manifest: &Manifest, _context: &Context) -> Result<ActionResult, ActionError> {
         let variant: PackageVariant = self.into();
         let box_provider = variant.provider.clone().get_provider();
diff --git a/src/actions/package/mod.rs b/src/actions/package/mod.rs
index 0a617510..f8b7e529 100644
--- a/src/actions/package/mod.rs
+++ b/src/actions/package/mod.rs
@@ -45,15 +45,10 @@ pub struct PackageVariant {
 
 impl PackageVariant {
     fn packages(&self) -> Vec<String> {
-        if self.name.is_some() {
-            return vec![self.name.clone().unwrap()];
-        }
-
-        if self.list.is_empty() {
-            return vec![];
-        }
-
-        self.list.clone()
+        self.name
+            .as_ref()
+            .map(|s| vec![s.clone()])
+            .unwrap_or_else(|| self.list.clone())
     }
 }
 
diff --git a/src/actions/package/providers/mod.rs b/src/actions/package/providers/mod.rs
index 137f9085..de4fcd79 100644
--- a/src/actions/package/providers/mod.rs
+++ b/src/actions/package/providers/mod.rs
@@ -34,6 +34,15 @@ impl PackageProviders {
             PackageProviders::Yay => Box::new(Yay {}),
         }
     }
+
+    pub fn name(&self) -> &'static str {
+        match self {
+            PackageProviders::Homebrew => "homebrew",
+            PackageProviders::Aptitude => "aptitude",
+            PackageProviders::Winget => "winget",
+            PackageProviders::Yay => "pacman",
+        }
+    }
 }
 
 impl Default for PackageProviders {
diff --git a/src/main.rs b/src/main.rs
index 4e17d82a..39e7933c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,4 @@
 mod actions;
-use actions::{Action, Actions};
 mod config;
 use crate::config::load_config;
 mod contexts;
@@ -21,6 +20,10 @@ use tracing_subscriber::FmtSubscriber;
 #[derive(StructOpt, Clone, Debug)]
 #[structopt(name = "comtrya")]
 pub struct Opt {
+    /// Performs a dry-run without changing the system
+    #[structopt(long)]
+    dry_run: bool,
+
     /// Debug & tracing mode (-v, -vv)
     #[structopt(short = "v", long = "verbose", parse(from_occurrences))]
     verbose: u8,
@@ -280,6 +283,7 @@ fn main() -> anyhow::Result<()> {
 
     debug!(manifests = run_manifests.join(",").as_str());
 
+    let dry_run = opt.dry_run;
     run_manifests.iter().for_each(|m| {
         let start = if m.eq(&String::from("")) {
             root_index
@@ -307,18 +311,14 @@ fn main() -> anyhow::Result<()> {
             let mut successful = true;
 
             m1.actions.iter().for_each(|action| {
-                let result = match action {
-                    Actions::CommandRun(a) => a.run(m1, &contexts),
-                    Actions::DirectoryCopy(a) => a.run(m1, &contexts),
-                    Actions::FileCopy(a) => a.run(m1, &contexts),
-                    Actions::FileLink(a) => a.run(m1, &contexts),
-                    Actions::PackageInstall(a) => a.run(&m1, &contexts),
+                let result = if dry_run {
+                    action.inner_ref().dry_run(&m1, &contexts)
+                } else {
+                    action.inner_ref().run(&m1, &contexts)
                 };
 
                 match result {
-                    Ok(result) => {
-                        debug!("{}", result.message)
-                    }
+                    Ok(result) => debug!("{}", result.message),
                     Err(e) => {
                         successful = false;
 


Test patch: