Show effective merged mounts in config command
Replace the per-layer mount display with a single 'Config volume mounts'
section showing the de-duplicated effective mounts, each tagged with the
scope it came from (default/user/project). Add a separate 'Config sources'
section listing which config files are present.

merged_mounts() now returns Vec<(Scope, &ResolvedMount)> so callers
can see which layer each mount originated from.
change pymyluzyumoqwvonktxnqlvsztonotpr
commit 3171ad146d350865d7c24ce8d38920b8f4e8a374
author Alpha Chen <alpha@kejadlen.dev>
date
parent toyqsowp
diff --git a/src/config.rs b/src/config.rs
index f60956d..738f7ce 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -64,7 +64,8 @@ impl ScopedConfig {
     ///
     /// Mounts accumulate across layers. When multiple layers define mounts with
     /// the same container target path, the higher-precedence layer wins.
-    pub fn merged_mounts(&self) -> Vec<&ResolvedMount> {
+    /// Each mount is tagged with the scope it came from.
+    pub fn merged_mounts(&self) -> Vec<(Scope, &ResolvedMount)> {
         let mut seen = std::collections::HashSet::new();
         let mut result = Vec::new();
         // Iterate in reverse (highest precedence first) so higher layers win,
@@ -72,7 +73,7 @@ impl ScopedConfig {
         for layer in self.layers.iter().rev() {
             for mount in layer.mounts.iter().rev() {
                 if seen.insert(&mount.target) {
-                    result.push(mount);
+                    result.push((layer.scope, mount));
                 }
             }
         }
@@ -449,8 +450,8 @@ mod tests {
         };
         let merged = config.merged_mounts();
         assert_eq!(merged.len(), 2);
-        assert_eq!(merged[0].target, "/a");
-        assert_eq!(merged[1].target, "/b");
+        assert_eq!(merged[0].1.target, "/a");
+        assert_eq!(merged[1].1.target, "/b");
     }
 
     #[test]
@@ -468,7 +469,7 @@ mod tests {
         };
         let merged = config.merged_mounts();
         assert_eq!(merged.len(), 1);
-        assert_eq!(merged[0].target, "/a");
+        assert_eq!(merged[0].1.target, "/a");
     }
 
     #[test]
@@ -506,9 +507,9 @@ mod tests {
         };
         let merged = config.merged_mounts();
         assert_eq!(merged.len(), 3);
-        assert_eq!(merged[0].target, "/a");
-        assert_eq!(merged[1].target, "/b");
-        assert_eq!(merged[2].target, "/c");
+        assert_eq!(merged[0].1.target, "/a");
+        assert_eq!(merged[1].1.target, "/b");
+        assert_eq!(merged[2].1.target, "/c");
     }
 
     #[test]
@@ -547,11 +548,13 @@ mod tests {
         // /root/.config/git appears in both layers; user layer wins
         assert_eq!(merged.len(), 2);
         // jj from default (not overridden)
-        assert_eq!(merged[0].target, "/root/.config/jj");
+        assert_eq!(merged[0].0, Scope::Default);
+        assert_eq!(merged[0].1.target, "/root/.config/jj");
         // git from user (overrides default)
-        assert_eq!(merged[1].target, "/root/.config/git");
-        assert_eq!(merged[1].source, PathBuf::from("/user/git"));
-        assert!(merged[1].writable);
+        assert_eq!(merged[1].0, Scope::User);
+        assert_eq!(merged[1].1.target, "/root/.config/git");
+        assert_eq!(merged[1].1.source, PathBuf::from("/user/git"));
+        assert!(merged[1].1.writable);
     }
 
     #[test]
diff --git a/src/main.rs b/src/main.rs
index 836d997..7888726 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -195,25 +195,30 @@ impl Ramekin {
             );
         }
 
+        println!();
+        println!("Config volume mounts");
+        let merged = self.config.merged_mounts();
+        if merged.is_empty() {
+            println!("  (none)");
+        } else {
+            for (scope, m) in &merged {
+                println!(
+                    "  {} {} → {}  ({})",
+                    check(&m.source),
+                    m.source.display(),
+                    m.display_target(),
+                    scope,
+                );
+            }
+        }
+
+        println!();
+        println!("Config sources");
         for layer in &self.config.layers {
-            println!();
-            let label = match layer.path {
-                Some(ref path) => format!("Volume mounts — {} ({})", layer.scope, path.display()),
-                None => format!("Volume mounts — {}", layer.scope),
+            match layer.path {
+                Some(ref path) => println!("  {} {} {}", check(path), layer.scope, path.display()),
+                None => println!("  ✓ {}", layer.scope),
             };
-            println!("{label}");
-            if layer.mounts.is_empty() {
-                println!("  (none)");
-            } else {
-                for m in &layer.mounts {
-                    println!(
-                        "  {} {} → {}",
-                        check(&m.source),
-                        m.source.display(),
-                        m.display_target()
-                    );
-                }
-            }
         }
 
         println!();
@@ -275,11 +280,14 @@ impl Ramekin {
             .create_cache_directory(format!("sessions/{session_id}"))
             .wrap_err("failed to create session directory")?;
 
-        let all_mounts: Vec<&config::ResolvedMount> = self
-            .builtin_mounts
-            .iter()
-            .chain(self.config.merged_mounts())
+        let config_mounts: Vec<_> = self
+            .config
+            .merged_mounts()
+            .into_iter()
+            .map(|(_, m)| m)
             .collect();
+        let all_mounts: Vec<&config::ResolvedMount> =
+            self.builtin_mounts.iter().chain(config_mounts).collect();
         let compose = generate_compose(&dockerfile, &build_context, &all_mounts);
         let compose_file = session_dir.join("compose.yml");
         fs_err::write(&compose_file, &compose)?;