use super::{
graph::{EdgeDirection, Graph, NodeIndex, Nodes},
Dependency,
};
use crate::{error::Error, lockfile::Lockfile, Map};
use std::{collections::BTreeSet as Set, io};
#[derive(Clone, Debug)]
pub struct Tree {
graph: Graph,
nodes: Nodes,
}
impl Tree {
pub fn new(lockfile: &Lockfile) -> Result<Self, Error> {
let mut graph = Graph::new();
let mut nodes = Map::new();
for package in &lockfile.packages {
let node_index = graph.add_node(package.clone());
nodes.insert(Dependency::from(package), node_index);
}
for package in &lockfile.packages {
let parent_index = nodes[&Dependency::from(package)];
for dependency in &package.dependencies {
if let Some(node_index) = nodes.get(dependency) {
graph.add_edge(parent_index, *node_index, dependency.clone());
}
}
}
Ok(Tree { graph, nodes })
}
pub fn render(
&self,
w: &mut impl io::Write,
node_index: NodeIndex,
direction: EdgeDirection,
exact: bool,
) -> io::Result<()> {
self.render_with_symbols(w, node_index, direction, &Symbols::default(), exact)
}
pub fn render_with_symbols(
&self,
w: &mut impl io::Write,
node_index: NodeIndex,
direction: EdgeDirection,
symbols: &Symbols,
exact: bool,
) -> io::Result<()> {
Presenter::new(&self.graph, symbols).print_node(w, node_index, direction, exact)
}
pub fn roots(&self) -> Vec<NodeIndex> {
self.graph.externals(EdgeDirection::Incoming).collect()
}
pub fn graph(&self) -> &Graph {
&self.graph
}
pub fn nodes(&self) -> &Nodes {
&self.nodes
}
}
pub struct Symbols {
down: &'static str,
tee: &'static str,
ell: &'static str,
right: &'static str,
}
impl Default for Symbols {
fn default() -> Symbols {
Self {
down: "│",
tee: "├",
ell: "└",
right: "─",
}
}
}
struct Presenter<'g, 's> {
graph: &'g Graph,
symbols: &'s Symbols,
levels_continue: Vec<bool>,
visited: Set<NodeIndex>,
}
impl<'g, 's> Presenter<'g, 's> {
fn new(graph: &'g Graph, symbols: &'s Symbols) -> Self {
Self {
graph,
symbols,
levels_continue: vec![],
visited: Set::new(),
}
}
fn print_node(
&mut self,
w: &mut impl io::Write,
node_index: NodeIndex,
direction: EdgeDirection,
exact: bool,
) -> io::Result<()> {
let package = &self.graph[node_index];
let new = self.visited.insert(node_index);
if let Some((&last_continues, rest)) = self.levels_continue.split_last() {
for &continues in rest {
let c = if continues { self.symbols.down } else { " " };
write!(w, "{} ", c)?;
}
let c = if last_continues {
self.symbols.tee
} else {
self.symbols.ell
};
write!(w, "{0}{1}{1} ", c, self.symbols.right)?;
}
if exact {
let spec = if let Some(checksum) = &package.checksum {
format!("checksum:{}", checksum)
} else if let Some(src) = &package.source {
src.to_string()
} else {
"inexact".to_string()
};
writeln!(w, "{} {} {}", &package.name, &package.version, spec)?;
} else {
writeln!(w, "{} {}", &package.name, &package.version)?;
}
if !new {
return Ok(());
}
use petgraph::visit::EdgeRef;
let dependencies = self
.graph
.edges_directed(node_index, direction)
.map(|edge| match direction {
EdgeDirection::Incoming => edge.source(),
EdgeDirection::Outgoing => edge.target(),
})
.collect::<Vec<_>>();
for (i, dependency) in dependencies.iter().enumerate() {
self.levels_continue.push(i < (dependencies.len() - 1));
self.print_node(w, *dependency, direction, exact)?;
self.levels_continue.pop();
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn load_lockfile() -> Lockfile {
Lockfile::load("tests/examples/Cargo.lock.v3").unwrap()
}
#[test]
fn compute_tree() {
Tree::new(&load_lockfile()).unwrap();
}
#[test]
fn compute_roots() {
let tree = Tree::new(&load_lockfile()).unwrap();
let roots = tree.roots();
assert_eq!(roots.len(), 1);
let root_package = &tree.graph[roots[0]];
assert_eq!(root_package.name.as_str(), "cargo-lock");
}
}