From 8f04a37eb011d2b26b43306ef5b372a931cbde09 Mon Sep 17 00:00:00 2001 From: peoplemiau1 Date: Fri, 15 May 2026 12:06:41 +0300 Subject: [PATCH 1/4] feat: add dcr tree command to visualize dependencies --- src/cli/help.rs | 1 + src/cli/mod.rs | 1 + src/cli/tree.rs | 117 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + 4 files changed, 120 insertions(+) create mode 100644 src/cli/tree.rs diff --git a/src/cli/help.rs b/src/cli/help.rs index 3601dbe..a3254d8 100644 --- a/src/cli/help.rs +++ b/src/cli/help.rs @@ -12,6 +12,7 @@ pub fn help() -> i32 { println!(" init Initialize the current directory as a project"); println!(" build [--profile] Build the project (default: --debug)"); println!(" run [--profile] Build and run the project (default: --debug)"); + println!(" tree Display the dependency tree"); println!(" test Run project tests (alias: tests)"); println!(" clean Remove the target directory"); println!(" gen Generate IDE integration files"); diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 6d9a93f..5ed19b4 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -10,3 +10,4 @@ pub mod new; pub mod run; pub mod setup; pub mod test; +pub mod tree; diff --git a/src/cli/tree.rs b/src/cli/tree.rs new file mode 100644 index 0000000..181771e --- /dev/null +++ b/src/cli/tree.rs @@ -0,0 +1,117 @@ +use crate::core::config::Config; +use crate::utils::log::error; +use crate::utils::text::{BOLD_CYAN, printc}; +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use toml::Value; + +pub fn tree(_args: &[String]) -> i32 { + let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + let config = match Config::open("dcr.toml") { + Ok(c) => c, + Err(_) => { + error("dcr.toml not found in current directory"); + return 1; + } + }; + + let name = config + .get("package.name") + .and_then(|v| v.as_str()) + .unwrap_or("unknown"); + let version = config + .get("package.version") + .and_then(|v| v.as_str()) + .unwrap_or("0.0.0"); + + printc(&format!("{} v{}", name, version), BOLD_CYAN); + + let mut seen = HashSet::new(); + seen.insert(name.to_string()); + + if let Some(deps) = config.get("dependencies").and_then(|v| v.as_table()) { + print_deps(deps, ¤t_dir, "", &mut seen); + } + + 0 +} + +fn print_deps(deps: &toml::value::Table, base_path: &Path, prefix: &str, seen: &mut HashSet) { + let mut dep_list: Vec<_> = deps.iter().collect(); + dep_list.sort_by_key(|(name, _)| *name); + + for (i, (name, value)) in dep_list.iter().enumerate() { + let is_last = i == dep_list.len() - 1; + let connector = if is_last { "└── " } else { "├── " }; + let child_prefix = if is_last { " " } else { "│ " }; + + let dep_path = resolve_dep_path(value, base_path); + let mut version = String::new(); + let mut sub_deps = None; + let mut resolved_path = None; + + if let Some(path) = &dep_path { + let dcr_toml = path.join("dcr.toml"); + if dcr_toml.exists() { + if let Ok(config) = Config::open(&dcr_toml.to_string_lossy()) { + if let Some(v) = config.get("package.version").and_then(|v| v.as_str()) { + version = format!(" v{}", v); + } + sub_deps = config.get("dependencies").and_then(|v| v.as_table()).cloned(); + resolved_path = Some(path.clone()); + } + } + } + + if version.is_empty() { + version = match value { + Value::String(s) if s.starts_with("git:") || s.starts_with("github:") => { + format!(" ({})", s) + } + Value::Table(t) => { + if let Some(v) = t.get("version").and_then(|v| v.as_str()) { + format!(" v{}", v) + } else if let Some(git) = t.get("git").and_then(|v| v.as_str()) { + format!(" ({})", git) + } else { + "".to_string() + } + } + _ => "".to_string(), + }; + } + + println!("{}{}{}{}", prefix, connector, name, version); + + if let Some(s_deps) = sub_deps { + if !seen.contains(*name) { + seen.insert(name.to_string()); + let new_prefix = format!("{}{}", prefix, child_prefix); + if let Some(path) = resolved_path { + print_deps(&s_deps, &path, &new_prefix, seen); + } + seen.remove(*name); + } + } + } +} + +fn resolve_dep_path(value: &Value, base_path: &Path) -> Option { + match value { + Value::String(s) => { + if s.starts_with("path:") { + Some(base_path.join(&s[5..])) + } else { + None + } + } + Value::Table(t) => { + if let Some(path) = t.get("path").and_then(|v| v.as_str()) { + Some(base_path.join(path)) + } else { + None + } + } + _ => None, + } +} diff --git a/src/main.rs b/src/main.rs index cb83903..805763b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,7 @@ fn main() { "add" => cli::add::add(rest), "build" => cli::build::build(rest), "run" => cli::run::run(rest), + "tree" => cli::tree::tree(rest), "test" | "tests" => cli::test::test(rest), "clean" => cli::clean::clean(rest), "gen" => cli::r#gen::r#gen(rest), From a123f647c85d928e3ea604730c563484760a92d3 Mon Sep 17 00:00:00 2001 From: peoplemiau1 Date: Fri, 15 May 2026 12:10:09 +0300 Subject: [PATCH 2/4] style: fix formatting in tree.rs --- src/cli/tree.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/cli/tree.rs b/src/cli/tree.rs index 181771e..9a9e6ae 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -36,7 +36,12 @@ pub fn tree(_args: &[String]) -> i32 { 0 } -fn print_deps(deps: &toml::value::Table, base_path: &Path, prefix: &str, seen: &mut HashSet) { +fn print_deps( + deps: &toml::value::Table, + base_path: &Path, + prefix: &str, + seen: &mut HashSet, +) { let mut dep_list: Vec<_> = deps.iter().collect(); dep_list.sort_by_key(|(name, _)| *name); @@ -57,7 +62,10 @@ fn print_deps(deps: &toml::value::Table, base_path: &Path, prefix: &str, seen: & if let Some(v) = config.get("package.version").and_then(|v| v.as_str()) { version = format!(" v{}", v); } - sub_deps = config.get("dependencies").and_then(|v| v.as_table()).cloned(); + sub_deps = config + .get("dependencies") + .and_then(|v| v.as_table()) + .cloned(); resolved_path = Some(path.clone()); } } From 9a24a80d59e7bd7109d0538edfd1d6bddbddbbdc Mon Sep 17 00:00:00 2001 From: peoplemiau1 Date: Fri, 15 May 2026 12:22:05 +0300 Subject: [PATCH 3/4] fix: address clippy warnings in tree.rs --- src/cli/tree.rs | 53 ++++++++++++++++++++----------------------------- 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/src/cli/tree.rs b/src/cli/tree.rs index 9a9e6ae..7db9bc3 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -57,17 +57,15 @@ fn print_deps( if let Some(path) = &dep_path { let dcr_toml = path.join("dcr.toml"); - if dcr_toml.exists() { - if let Ok(config) = Config::open(&dcr_toml.to_string_lossy()) { - if let Some(v) = config.get("package.version").and_then(|v| v.as_str()) { - version = format!(" v{}", v); - } - sub_deps = config - .get("dependencies") - .and_then(|v| v.as_table()) - .cloned(); - resolved_path = Some(path.clone()); + if dcr_toml.exists() && let Ok(config) = Config::open(&dcr_toml.to_string_lossy()) { + if let Some(v) = config.get("package.version").and_then(|v| v.as_str()) { + version = format!(" v{}", v); } + sub_deps = config + .get("dependencies") + .and_then(|v| v.as_table()) + .cloned(); + resolved_path = Some(path.clone()); } } @@ -91,35 +89,26 @@ fn print_deps( println!("{}{}{}{}", prefix, connector, name, version); - if let Some(s_deps) = sub_deps { - if !seen.contains(*name) { - seen.insert(name.to_string()); - let new_prefix = format!("{}{}", prefix, child_prefix); - if let Some(path) = resolved_path { - print_deps(&s_deps, &path, &new_prefix, seen); - } - seen.remove(*name); + if let Some(s_deps) = sub_deps && !seen.contains(*name) { + seen.insert(name.to_string()); + let new_prefix = format!("{}{}", prefix, child_prefix); + if let Some(path) = resolved_path { + print_deps(&s_deps, &path, &new_prefix, seen); } + seen.remove(*name); } } } fn resolve_dep_path(value: &Value, base_path: &Path) -> Option { match value { - Value::String(s) => { - if s.starts_with("path:") { - Some(base_path.join(&s[5..])) - } else { - None - } - } - Value::Table(t) => { - if let Some(path) = t.get("path").and_then(|v| v.as_str()) { - Some(base_path.join(path)) - } else { - None - } - } + Value::String(s) => s + .strip_prefix("path:") + .map(|stripped| base_path.join(stripped)), + Value::Table(t) => t + .get("path") + .and_then(|v| v.as_str()) + .map(|path| base_path.join(path)), _ => None, } } From 954f2870ba69191d32d466586ff7efe5a412ab97 Mon Sep 17 00:00:00 2001 From: peoplemiau1 Date: Fri, 15 May 2026 12:39:28 +0300 Subject: [PATCH 4/4] style: final formatting and clippy fixes for tree.rs --- src/cli/tree.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/cli/tree.rs b/src/cli/tree.rs index 7db9bc3..b38a9c4 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -57,7 +57,9 @@ fn print_deps( if let Some(path) = &dep_path { let dcr_toml = path.join("dcr.toml"); - if dcr_toml.exists() && let Ok(config) = Config::open(&dcr_toml.to_string_lossy()) { + if dcr_toml.exists() + && let Ok(config) = Config::open(&dcr_toml.to_string_lossy()) + { if let Some(v) = config.get("package.version").and_then(|v| v.as_str()) { version = format!(" v{}", v); } @@ -89,7 +91,9 @@ fn print_deps( println!("{}{}{}{}", prefix, connector, name, version); - if let Some(s_deps) = sub_deps && !seen.contains(*name) { + if let Some(s_deps) = sub_deps + && !seen.contains(*name) + { seen.insert(name.to_string()); let new_prefix = format!("{}{}", prefix, child_prefix); if let Some(path) = resolved_path {