Repository: nickgerace/gfold (language: rust)

Issue:
way to specify multiple directories via config?
Based on the README, seems like - whether from shell or config - only able to specify single path:

> By default, gfold looks at every Git repository via traversal from the current working directory. If you would like to target another directory, you can pass its path (relative or absolute) as the first argument or change the default path in the config file.

```toml
path = '/home/neloth'
```

My first thought on discovering this repo (h/t https://terminaltrove.com/) is it would be great for:

* specifying a list of repos I'm working with daily, especially those that exist on multiple machines and for which I want to avoid merge hell
* run gfold before leaving for/from the office and see "do I need to commit anything?"

Anyhow, keep up the good work and great project!


Discussion / hints:
This is a good idea! Yes, it only looks at one path now, but I think allowing for multiple paths is a good idea. I'll mark it as "seeking contribution" for now, but may tackle it myself.

And thank you for telling me where you found `gfold` from! It helps a lot.
@nickgerace I would be happy to work on this. I believe we should modify both the CLI and the config file specification for the same.

I am thinking of the following usage for the cli - 

`gfold path1 path2 path3`,

and for the config file 

`paths = ["path1", "path2", "path3"]`.

I am not able to figure out a way to implement this functionality in a backwards compatible way. If you or @zachvalenta have any ideas let me know. 


@SoutrikBandyopadhyay thank you! I'd have both the `path` and `paths` option in the config and return an error if the user provides both. It's not the most elegant solution, but I am planning a `5.0.0` release where we can drop the old `path` option depending on how this goes too.
I think both could be supported in a backwards-compatible way. If `path` is supplied from config file, print a deprecation warning and then put the single path into a vec, like `paths` would be. (`path` and `paths` would be mutually exclusive, so an error in that scenario as well.) The CLI can start accepting multiple `[PATHS]...`, which is still backwards compatible with a single `[PATH]`. At a later date `path` support could be removed in a major update as mentioned earlier.

Fix patch:
diff --git a/README.md b/README.md
index 11ce71b..cc147f6 100644
--- a/README.md
+++ b/README.md
@@ -72,7 +72,7 @@ gfold -d classic -c never ~/ --dry-run > $HOME/.config/gfold.toml
 Here are the contents of the resulting config file:
 
 ```toml
-path = '/home/neloth'
+paths = ['/home/neloth']
 display_mode = 'Classic'
 color_mode = 'Never'
 ```
diff --git a/src/config.rs b/src/config.rs
index 575f9f2..36d638e 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -11,8 +11,8 @@ use std::{env, fs};
 /// deserialize empty, non-existent, partial, and complete config files.
 #[derive(Serialize)]
 pub struct Config {
-    /// The path that `gfold` will begin traversal in and collect results from.
-    pub path: PathBuf,
+    /// The paths that `gfold` will traverse and collect results from.
+    pub paths: Vec<PathBuf>,
     /// The display format for results printed to `stdout`.
     pub display_mode: DisplayMode,
     /// The color mode for results printed to `stdout`.
@@ -63,10 +63,24 @@ impl Config {
     }
 
     fn from_entry_config(entry_config: &EntryConfig) -> Result<Self> {
+        if entry_config.path.is_some() && entry_config.paths.is_some() {
+            return Err(anyhow::anyhow!(
+                "Cannot have both `path` and `paths` in config"
+            ));
+        }
         Ok(Config {
-            path: match &entry_config.path {
-                Some(path) => normalize_path(path)?,
-                None => env::current_dir()?.canonicalize()?,
+            paths: if let Some(paths) = &entry_config.paths {
+                paths
+                    .iter()
+                    .map(|p| normalize_path(p))
+                    .collect::<Result<Vec<PathBuf>, _>>()?
+            } else if let Some(path) = &entry_config.path {
+                eprintln!(
+                    "WARNING: the `path` configuration option is deprecated. Use `paths` instead."
+                );
+                vec![normalize_path(path)?]
+            } else {
+                vec![env::current_dir()?.canonicalize()?]
             },
             display_mode: match &entry_config.display_mode {
                 Some(display_mode) => *display_mode,
@@ -98,8 +112,11 @@ fn normalize_path(path: &Path) -> Result<PathBuf> {
 /// this struct privately.
 #[derive(Deserialize, Default)]
 struct EntryConfig {
-    /// Reflection of the `path` field on [`Config`].
+    /// Formerly a reflection of the `path` field on [`Config`]. Use `paths` instead.
+    /// This field is deprecated and will be removed in a future release.
     pub path: Option<PathBuf>,
+    /// Reflection of the `paths` field on [`Config`].
+    pub paths: Option<Vec<PathBuf>>,
     /// Reflection of the `display_mode` field on [`Config`].
     pub display_mode: Option<DisplayMode>,
     /// Reflection of the `color_mode` field on [`Config`].
diff --git a/src/main.rs b/src/main.rs
index 3e3c832..d92c71a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -11,7 +11,7 @@ use gfold::RepositoryCollector;
 use clap::Parser;
 use env_logger::Builder;
 use log::{debug, LevelFilter};
-use std::env;
+use std::{env, path::PathBuf};
 
 mod config;
 mod display;
@@ -33,7 +33,7 @@ Troubleshooting: investigate unexpected behavior by prepending execution with \"
 #[command(version, about = HELP, long_about = None)]
 struct Cli {
     /// specify path to target directory (defaults to current working directory)
-    pub path: Option<String>,
+    pub paths: Option<Vec<PathBuf>>,
 
     #[arg(short, long)]
     pub color_mode: Option<ColorMode>,
@@ -73,8 +73,12 @@ fn main() -> anyhow::Result<()> {
     if let Some(found_color_mode) = &cli.color_mode {
         config.color_mode = *found_color_mode;
     }
-    if let Some(found_path) = &cli.path {
-        config.path = env::current_dir()?.join(found_path).canonicalize()?;
+    if let Some(found_paths) = &cli.paths {
+        let current_dir = env::current_dir()?;
+        config.paths = found_paths
+            .iter()
+            .map(|p| current_dir.join(p).canonicalize())
+            .collect::<Result<Vec<PathBuf>, _>>()?;
     }
     debug!("finalized config options");
 
@@ -86,10 +90,14 @@ fn main() -> anyhow::Result<()> {
             DisplayMode::Json => (true, true),
             DisplayMode::Standard | DisplayMode::StandardAlphabetical => (true, false),
         };
-        let repository_collection =
-            RepositoryCollector::run(&config.path, include_email, include_submodules)?;
-        let display_harness = DisplayHarness::new(config.display_mode, config.color_mode);
-        display_harness.run(&repository_collection)?;
+        for path in &config.paths {
+            debug!("processing path: {:?}", path);
+
+            let repository_collection =
+                RepositoryCollector::run(path, include_email, include_submodules)?;
+            let display_harness = DisplayHarness::new(config.display_mode, config.color_mode);
+            display_harness.run(&repository_collection)?;
+        }
     }
     Ok(())
 }


Test patch: