Add dependency graph to registry

Change-Id: I6915a82af17bdb256a26fff5e6067c68ee19e3d8
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/qg/+/125198
Reviewed-by: Alexei Frolov <frolv@google.com>
Commit-Queue: Erik Gilling <konkers@google.com>
diff --git a/qg-cli/src/graph.rs b/qg-cli/src/graph.rs
new file mode 100644
index 0000000..d311063
--- /dev/null
+++ b/qg-cli/src/graph.rs
@@ -0,0 +1,30 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+use anyhow::Result;
+use qg::Project;
+use tokio::io::{self, AsyncWriteExt};
+
+/// Dump the dependency graph in graphviz dot format.
+#[derive(clap::Parser, Debug)]
+#[command(about)]
+pub struct Command {}
+
+pub async fn run(_args: &Command) -> Result<()> {
+    let project = Project::locate()?;
+    let graph = project.registry()?.dot_dependency_graph();
+
+    io::stdout().write_all(graph.as_bytes()).await?;
+    Ok(())
+}
diff --git a/qg-cli/src/main.rs b/qg-cli/src/main.rs
index ea1a6d8..41a2414 100644
--- a/qg-cli/src/main.rs
+++ b/qg-cli/src/main.rs
@@ -16,6 +16,7 @@
 #![cfg_attr(feature = "strict", deny(warnings))]
 #![warn(clippy::pedantic)]
 
+mod graph;
 mod hello;
 mod subcommands;
 mod target;
diff --git a/qg-cli/src/subcommands.rs b/qg-cli/src/subcommands.rs
index 39bdf66..8d5f42b 100644
--- a/qg-cli/src/subcommands.rs
+++ b/qg-cli/src/subcommands.rs
@@ -14,7 +14,7 @@
 
 use anyhow::Result;
 
-use crate::{hello, target};
+use crate::{graph, hello, target};
 
 #[cfg(feature = "new_command")]
 use crate::new;
@@ -28,6 +28,7 @@
 /// struct which derives `clap::Parser` to specify its arguments.
 #[derive(clap::Parser, Debug)]
 pub enum Subcommands {
+    Graph(graph::Command),
     Hello(hello::Command),
     Target(target::Command),
 
@@ -42,6 +43,9 @@
     /// Invokes a subcommand with its parsed arguments.
     pub async fn run(&self) -> Result<()> {
         match self {
+            Self::Graph(args) => {
+                graph::run(args).await?;
+            }
             Self::Hello(args) => {
                 hello::run(args);
             }
diff --git a/qg/Cargo.toml b/qg/Cargo.toml
index 182b32f..cb2a8c2 100644
--- a/qg/Cargo.toml
+++ b/qg/Cargo.toml
@@ -11,6 +11,7 @@
 futures = "0.3.25"
 num-traits = "0.2.15"
 once_cell = "1.16.0"
+petgraph = "0.6.2"
 regex = "1.7.0"
 serde = { version = "1.0.147", features = ["derive"] }
 thiserror = "1.0.37"
diff --git a/qg/src/project/mod.rs b/qg/src/project/mod.rs
index 8fe7bb9..af14659 100644
--- a/qg/src/project/mod.rs
+++ b/qg/src/project/mod.rs
@@ -271,6 +271,8 @@
 
 #[cfg(test)]
 mod tests {
+    use std::collections::HashSet;
+
     use super::*;
 
     #[test]
@@ -354,4 +356,31 @@
         // Loads from a project subdirectory fail.
         assert!(Project::load("./src/test_projects/simple/subdir/subsubdir").is_err());
     }
+
+    #[test]
+    fn simple_dependencies() {
+        let project = Project::load("./src/test_projects/dependency_test").unwrap();
+        assert_eq!(project.name, "dep-test");
+        let registry = project.registry().unwrap();
+        let a_id = registry.get_node_id("dep-test:a").unwrap();
+        let b_id = registry.get_node_id("dep-test:b").unwrap();
+        let c_id = registry.get_node_id("dep-test:c").unwrap();
+        let d_id = registry.get_node_id("dep-test:d").unwrap();
+        let e_id = registry.get_node_id("dep-test:e").unwrap();
+
+        let a_deps: HashSet<_> = registry.node_deps(a_id).collect();
+        assert_eq!(a_deps, [b_id, c_id].into_iter().collect());
+
+        let b_deps: HashSet<_> = registry.node_deps(b_id).collect();
+        assert_eq!(b_deps, [].into_iter().collect());
+
+        let c_deps: HashSet<_> = registry.node_deps(c_id).collect();
+        assert_eq!(c_deps, [d_id, e_id].into_iter().collect());
+
+        let d_deps: HashSet<_> = registry.node_deps(d_id).collect();
+        assert_eq!(d_deps, [].into_iter().collect());
+
+        let e_deps: HashSet<_> = registry.node_deps(e_id).collect();
+        assert_eq!(e_deps, [].into_iter().collect());
+    }
 }
diff --git a/qg/src/registry.rs b/qg/src/registry.rs
index 8c3aa74..b75812d 100644
--- a/qg/src/registry.rs
+++ b/qg/src/registry.rs
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
 //
 // Licensed under the Apache License, Version 2.0 (the "License"); you may not
 // use this file except in compliance with the License. You may obtain a copy of
@@ -14,12 +14,52 @@
 
 use std::{
     collections::{HashMap, HashSet},
+    fmt,
     sync::Arc,
 };
 
+use petgraph::{
+    dot::{self, Dot},
+    graph::{Neighbors, NodeIndex},
+    Directed, Direction, Graph,
+};
+
 use crate::target::{Provider, Target};
 use crate::{Error, Result};
 
+/// A node in the dependency graph.
+#[derive(Debug)]
+pub enum Dependency {
+    /// A resolved dependency.
+    Resolved(Arc<Target>),
+
+    /// An unresolved dependency.  This occurs when a target declares a
+    /// dependency for a target that has not been seen.
+    Unresolved(String),
+}
+
+impl fmt::Display for Dependency {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Resolved(target) => write!(f, "{}", target.full_name()),
+            Self::Unresolved(name) => write!(f, "{name}"),
+        }
+    }
+}
+
+/// An edge in the dependency graph.
+/// We define an empty struct for edges instead of using the unit type
+/// to allow us to implement `fmt::Display` for it.  This is needed for
+/// graphviz output.
+#[derive(Debug)]
+pub struct Edge {}
+
+impl fmt::Display for Edge {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "")
+    }
+}
+
 /// A database of packages known to `qg`.
 #[derive(Debug)]
 pub struct Registry {
@@ -31,6 +71,14 @@
 
     /// Unknown target dependencies.
     unresolved_dependencies: HashSet<String>,
+
+    /// Dependency Graph of targets.
+    ///
+    /// Nodes that have a `None` weight are unresolved.
+    dependency_graph: Graph<Dependency, Edge, Directed, u32>,
+
+    /// Map from target full name to dependency graph node IDe.
+    node_id_map: HashMap<String, NodeIndex<u32>>,
 }
 
 impl Registry {
@@ -41,6 +89,8 @@
             targets: HashMap::new(),
             providers: HashMap::new(),
             unresolved_dependencies: HashSet::new(),
+            dependency_graph: Graph::new(),
+            node_id_map: HashMap::new(),
         }
     }
 
@@ -65,6 +115,15 @@
         Ok(())
     }
 
+    /// Returns the dependency graph in graphviz dot format.
+    #[must_use]
+    pub fn dot_dependency_graph(&self) -> String {
+        format!(
+            "{:}",
+            Dot::with_config(&self.dependency_graph, &[dot::Config::EdgeNoLabel])
+        )
+    }
+
     /// Inserts a target into the registry.
     pub(crate) fn add_target(&mut self, target: Target) -> Result<()> {
         if !self.providers.contains_key(target.provider()) {
@@ -73,7 +132,6 @@
         };
 
         let target_name = target.full_name();
-
         for dep in target
             .dependencies()
             .filter(|&d| !self.targets.contains_key(d))
@@ -84,10 +142,64 @@
         self.unresolved_dependencies.remove(&target_name);
 
         let target = Arc::new(target);
-        self.targets.insert(target_name, target);
+        self.update_dependency_graph_for_new_target(&target);
+        self.targets.insert(target_name.clone(), target);
 
         Ok(())
     }
+
+    fn update_dependency_graph_for_new_target(&mut self, target: &Arc<Target>) {
+        let target_name = target.full_name();
+
+        let node_id = if let Some(id) = self.node_id_map.get(&target_name) {
+            // If the target is already in the graph, it is because a previous
+            // target as taken an unresolved dependency on it.  Update the node
+            // with the resolved target.
+            *self
+                .dependency_graph
+                .node_weight_mut(*id)
+                .expect("node should exist in graph") = Dependency::Resolved(target.clone());
+            *id
+        } else {
+            let id = self
+                .dependency_graph
+                .add_node(Dependency::Resolved(target.clone()));
+            self.node_id_map.insert(target_name, id);
+            id
+        };
+
+        for dep in target.dependencies() {
+            // Get the node id of the dependency.  Insert an unresolved
+            // dependency if the node does not already exist.
+            let dep_node_id = *self.node_id_map.entry(dep.to_string()).or_insert_with(|| {
+                self.dependency_graph
+                    .add_node(Dependency::Unresolved(dep.to_string()))
+            });
+            self.dependency_graph
+                .add_edge(node_id, dep_node_id, Edge {});
+        }
+    }
+
+    /// Returns the node ID for a node.
+    ///
+    /// # Errors
+    /// Returns an error if the given node name does not exist.
+    pub fn get_node_id(&self, name: &str) -> Result<NodeIndex<u32>> {
+        self.node_id_map
+            .get(name)
+            .ok_or(
+                // Node not found.
+                Error::GenericErrorPlaceholder,
+            )
+            .map(|id| *id)
+    }
+
+    /// Returns an iterator of dependencies for the node.
+    #[must_use]
+    pub fn node_deps(&self, node: NodeIndex<u32>) -> Neighbors<Edge, u32> {
+        self.dependency_graph
+            .neighbors_directed(node, Direction::Outgoing)
+    }
 }
 
 impl Default for Registry {
diff --git a/qg/src/test_projects/dependency_test/qg.toml b/qg/src/test_projects/dependency_test/qg.toml
new file mode 100644
index 0000000..b5f2d0c
--- /dev/null
+++ b/qg/src/test_projects/dependency_test/qg.toml
@@ -0,0 +1,25 @@
+[project]
+name = "dep-test"
+
+#      a
+#     / \
+#    b  c
+#      / \
+#     d  e
+
+[targets.a]
+type = "dep_only"
+deps = ["dep-test:b", "dep-test:c"]
+
+[targets.b]
+type = "dep_only"
+
+[targets.c]
+type = "dep_only"
+deps = ["dep-test:d", "dep-test:e"]
+
+[targets.d]
+type = "dep_only"
+
+[targets.e]
+type = "dep_only"
diff --git a/qg/src/test_projects/dependency_test/subdir/subsubdir/keepdir b/qg/src/test_projects/dependency_test/subdir/subsubdir/keepdir
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/qg/src/test_projects/dependency_test/subdir/subsubdir/keepdir