Skip to main content

zoi_cli/cmd/
tree.rs

1//! Logic for the `tree` command.
2
3use std::collections::HashSet;
4
5use anyhow::{Result, anyhow};
6use colored::Colorize;
7
8use crate::pkg::install::resolver;
9
10/// Runs the `tree` command to display the dependency tree of one or more
11/// packages.
12///
13/// # Errors
14///
15/// Returns an error if the dependency tree cannot be resolved.
16pub fn run(package_names: &[String]) -> Result<()> {
17    if package_names.is_empty() {
18        println!("{}", "Please specify at least one package name.".yellow());
19        return Ok(());
20    }
21
22    println!("{} Resolving dependency tree...", "::".bold().blue());
23
24    let (graph, non_zoi_deps) = resolver::resolve_dependency_graph(
25        package_names,
26        None,
27        false,
28        true,
29        true,
30        None,
31        true,
32        None
33    )?;
34
35    if !non_zoi_deps.is_empty() {
36        println!(
37            "\n{} External dependencies (non-Zoi):",
38            "::".bold().yellow()
39        );
40        for dep in non_zoi_deps {
41            println!("  - {}", dep.dimmed());
42        }
43    }
44
45    println!("\n{} Dependency tree:", "::".bold().blue());
46
47    let mut visited = HashSet::new();
48    for source in package_names {
49        if let Some(children) = graph.adj.get("$root") {
50            for pkg_id in children {
51                let Some(node) = graph.nodes.get(pkg_id) else {
52                    continue;
53                };
54                if source.contains(&node.pkg.name) {
55                    print_node(&graph, pkg_id, "", true, &mut visited)?;
56                }
57            }
58        }
59    }
60
61    Ok(())
62}
63
64/// Recursively prints a node in the dependency tree.
65fn print_node(
66    graph: &resolver::DependencyGraph,
67    pkg_id: &str,
68    prefix: &str,
69    is_last: bool,
70    visited: &mut HashSet<String>
71) -> Result<()> {
72    let node = graph
73        .nodes
74        .get(pkg_id)
75        .ok_or_else(|| anyhow!("Package not found in graph: {pkg_id}"))?;
76    let is_repeated = visited.contains(pkg_id);
77
78    let connector = if is_last { "└── " } else { "├── " };
79
80    let pkg_display = if let Some(sub) = &node.sub_package {
81        format!("{}:{}", node.pkg.name.cyan().bold(), sub.yellow())
82    } else {
83        node.pkg.name.cyan().bold().to_string()
84    };
85
86    let version_display = format!("v{}", node.version);
87    let repeated_mark = if is_repeated {
88        " (*)".dimmed()
89    } else {
90        "".normal()
91    };
92
93    println!(
94        "{prefix}{connector}{pkg_display}{repeated_mark} {version_display}"
95    );
96
97    if is_repeated {
98        return Ok(());
99    }
100    visited.insert(pkg_id.to_string());
101
102    if let Some(children) = graph.adj.get(pkg_id) {
103        let child_count = children.len();
104        let mut sorted_children: Vec<_> = children.iter().collect();
105        sorted_children.sort();
106
107        for (i, child_id) in sorted_children.iter().enumerate() {
108            let new_prefix =
109                format!("{}{}", prefix, if is_last { "    " } else { "│   " });
110            print_node(
111                graph,
112                child_id,
113                &new_prefix,
114                i == child_count - 1,
115                visited
116            )?;
117        }
118    }
119
120    Ok(())
121}