Repository: rust-lang/cargo (rust)

Issue:
Option for `cargo tree` to filter out (non-)workspace members
### Problem

I'm opening this issue because https://github.com/rust-lang/cargo/issues/8105#issuecomment-1284860085 said to open one if one wanted that feature.

`cargo tree` allows with `--prune` to remove dependencies from the list. However, as mentioned in https://github.com/rust-lang/cargo/issues/8105#issuecomment-846336129 it only takes in `SPEC`, and not the proposed "non-members" or "members".

This would be useful to have, since it would allow easily viewing the dependency tree with only workspace members.

### Proposed Solution

The solution would be adding some argument, that would then take the values `both`, `member`, and `non-member` (as mentioned in https://github.com/rust-lang/cargo/issues/8105#issuecomment-846336129).

I have no proposed name yet for the argument.

### Notes

At the time of writing, there does not exist any such command, see https://github.com/rust-lang/cargo/blob/ba8b39413c74d08494f94a7542fe79aa636e1661/src/doc/src/commands/cargo-tree.md for the `cargo tree` documentation at the time of writing.

Pull request description:
feat: Implement `--depth workspace` for `cargo tree` command
Resolves #14420 

Patch:
diff --git a/src/bin/cargo/commands/tree.rs b/src/bin/cargo/commands/tree.rs
index b0f35370e..98b21db6c 100644
--- a/src/bin/cargo/commands/tree.rs
+++ b/src/bin/cargo/commands/tree.rs
@@ -2,7 +2,7 @@ use crate::cli;
 use crate::command_prelude::*;
 use anyhow::{bail, format_err};
 use cargo::core::dependency::DepKind;
-use cargo::ops::tree::{self, EdgeKind};
+use cargo::ops::tree::{self, DisplayDepth, EdgeKind};
 use cargo::ops::Packages;
 use cargo::util::print_available_packages;
 use cargo::util::CargoResult;
@@ -162,6 +162,12 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
 
     let pkgs_to_prune = args._values_of("prune");
 
+    let display_depth = args
+        ._value_of("depth")
+        .map(|s| s.parse::<DisplayDepth>())
+        .transpose()?
+        .unwrap_or(DisplayDepth::MaxDisplayDepth(u32::MAX));
+
     let packages = args.packages_from_flags()?;
     let mut invert = args
         .get_many::<String>("invert")
@@ -222,7 +228,7 @@ subtree of the package given to -p.\n\
         duplicates: args.flag("duplicates"),
         format: args.get_one::<String>("format").cloned().unwrap(),
         graph_features,
-        max_display_depth: args.value_of_u32("depth")?.unwrap_or(u32::MAX),
+        display_depth,
         no_proc_macro,
     };
 
diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs
index a2345cf67..a81c6b930 100644
--- a/src/cargo/core/workspace.rs
+++ b/src/cargo/core/workspace.rs
@@ -591,6 +591,11 @@ impl<'gctx> Workspace<'gctx> {
         self.member_ids.contains(&pkg.package_id())
     }
 
+    /// Returns true if the given package_id is a member of the workspace.
+    pub fn is_member_id(&self, package_id: PackageId) -> bool {
+        self.member_ids.contains(&package_id)
+    }
+
     pub fn is_ephemeral(&self) -> bool {
         self.is_ephemeral
     }
diff --git a/src/cargo/ops/tree/mod.rs b/src/cargo/ops/tree/mod.rs
index 0940f0ebf..e0579696f 100644
--- a/src/cargo/ops/tree/mod.rs
+++ b/src/cargo/ops/tree/mod.rs
@@ -6,7 +6,7 @@ use crate::core::dependency::DepKind;
 use crate::core::resolver::{features::CliFeatures, ForceAllTargets, HasDevUnits};
 use crate::core::{Package, PackageId, PackageIdSpec, PackageIdSpecQuery, Workspace};
 use crate::ops::{self, Packages};
-use crate::util::{CargoResult, GlobalContext};
+use crate::util::CargoResult;
 use crate::{drop_print, drop_println};
 use anyhow::Context as _;
 use graph::Graph;
@@ -43,8 +43,10 @@ pub struct TreeOptions {
     pub format: String,
     /// Includes features in the tree as separate nodes.
     pub graph_features: bool,
-    /// Maximum display depth of the dependency tree.
-    pub max_display_depth: u32,
+    /// Display depth of the dependency tree.
+    /// If non-negative integer, display dependencies with that amount of max depth.
+    /// If `workspace`, display dependencies from current workspace only.
+    pub display_depth: DisplayDepth,
     /// Excludes proc-macro dependencies.
     pub no_proc_macro: bool,
 }
@@ -86,6 +88,32 @@ impl FromStr for Prefix {
     }
 }
 
+#[derive(Clone, Copy)]
+pub enum DisplayDepth {
+    MaxDisplayDepth(u32),
+    Workspace,
+}
+
+impl FromStr for DisplayDepth {
+    type Err = clap::Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s {
+            "workspace" => Ok(Self::Workspace),
+            s => s.parse().map(Self::MaxDisplayDepth).map_err(|_| {
+                clap::Error::raw(
+                    clap::error::ErrorKind::ValueValidation,
+                    format!(
+                        "supported values for --depth are non-negative integers and `workspace`, \
+                                but `{}` is unknown",
+                        s
+                    ),
+                )
+            }),
+        }
+    }
+}
+
 struct Symbols {
     down: &'static str,
     tee: &'static str,
@@ -203,14 +231,14 @@ pub fn build_and_print(ws: &Workspace<'_>, opts: &TreeOptions) -> CargoResult<()
         try to use option `--target all` first, and then narrow your search scope accordingly.",
         )?;
     } else {
-        print(ws.gctx(), opts, root_indexes, &pkgs_to_prune, &graph)?;
+        print(ws, opts, root_indexes, &pkgs_to_prune, &graph)?;
     }
     Ok(())
 }
 
 /// Prints a tree for each given root.
 fn print(
-    gctx: &GlobalContext,
+    ws: &Workspace<'_>,
     opts: &TreeOptions,
     roots: Vec<usize>,
     pkgs_to_prune: &[PackageIdSpec],
@@ -219,7 +247,7 @@ fn print(
     let format = Pattern::new(&opts.format)
         .with_context(|| format!("tree format `{}` not valid", opts.format))?;
 
-    let symbols = if gctx.shell().out_unicode() {
+    let symbols = if ws.gctx().shell().out_unicode() {
         &UTF8_SYMBOLS
     } else {
         &ASCII_SYMBOLS
@@ -231,7 +259,7 @@ fn print(
 
     for (i, root_index) in roots.into_iter().enumerate() {
         if i != 0 {
-            drop_println!(gctx);
+            drop_println!(ws.gctx());
         }
 
         // A stack of bools used to determine where | symbols should appear
@@ -242,7 +270,7 @@ fn print(
         let mut print_stack = vec![];
 
         print_node(
-            gctx,
+            ws,
             graph,
             root_index,
             &format,
@@ -250,7 +278,7 @@ fn print(
             pkgs_to_prune,
             opts.prefix,
             opts.no_dedupe,
-            opts.max_display_depth,
+            opts.display_depth,
             &mut visited_deps,
             &mut levels_continue,
             &mut print_stack,
@@ -262,7 +290,7 @@ fn print(
 
 /// Prints a package and all of its dependencies.
 fn print_node<'a>(
-    gctx: &GlobalContext,
+    ws: &Workspace<'_>,
     graph: &'a Graph<'_>,
     node_index: usize,
     format: &Pattern,
@@ -270,7 +298,7 @@ fn print_node<'a>(
     pkgs_to_prune: &[PackageIdSpec],
     prefix: Prefix,
     no_dedupe: bool,
-    max_display_depth: u32,
+    display_depth: DisplayDepth,
     visited_deps: &mut HashSet<usize>,
     levels_continue: &mut Vec<bool>,
     print_stack: &mut Vec<usize>,
@@ -278,12 +306,12 @@ fn print_node<'a>(
     let new = no_dedupe || visited_deps.insert(node_index);
 
     match prefix {
-        Prefix::Depth => drop_print!(gctx, "{}", levels_continue.len()),
+        Prefix::Depth => drop_print!(ws.gctx(), "{}", levels_continue.len()),
         Prefix::Indent => {
             if let Some((last_continues, rest)) = levels_continue.split_last() {
                 for continues in rest {
                     let c = if *continues { symbols.down } else { " " };
-                    drop_print!(gctx, "{}   ", c);
+                    drop_print!(ws.gctx(), "{}   ", c);
                 }
 
                 let c = if *last_continues {
@@ -291,7 +319,7 @@ fn print_node<'a>(
                 } else {
                     symbols.ell
                 };
-                drop_print!(gctx, "{0}{1}{1} ", c, symbols.right);
+                drop_print!(ws.gctx(), "{0}{1}{1} ", c, symbols.right);
             }
         }
         Prefix::None => {}
@@ -307,7 +335,7 @@ fn print_node<'a>(
     } else {
         " (*)"
     };
-    drop_println!(gctx, "{}{}", format.display(graph, node_index), star);
+    drop_println!(ws.gctx(), "{}{}", format.display(graph, node_index), star);
 
     if !new || in_cycle {
         return;
@@ -321,7 +349,7 @@ fn print_node<'a>(
         EdgeKind::Feature,
     ] {
         print_dependencies(
-            gctx,
+            ws,
             graph,
             node_index,
             format,
@@ -329,7 +357,7 @@ fn print_node<'a>(
             pkgs_to_prune,
             prefix,
             no_dedupe,
-            max_display_depth,
+            display_depth,
             visited_deps,
             levels_continue,
             print_stack,
@@ -341,7 +369,7 @@ fn print_node<'a>(
 
 /// Prints all the dependencies of a package for the given dependency kind.
 fn print_dependencies<'a>(
-    gctx: &GlobalContext,
+    ws: &Workspace<'_>,
     graph: &'a Graph<'_>,
     node_index: usize,
     format: &Pattern,
@@ -349,7 +377,7 @@ fn print_dependencies<'a>(
     pkgs_to_prune: &[PackageIdSpec],
     prefix: Prefix,
     no_dedupe: bool,
-    max_display_depth: u32,
+    display_depth: DisplayDepth,
     visited_deps: &mut HashSet<usize>,
     levels_continue: &mut Vec<bool>,
     print_stack: &mut Vec<usize>,
@@ -371,13 +399,18 @@ fn print_dependencies<'a>(
         if let Some(name) = name {
             for continues in &**levels_continue {
                 let c = if *continues { symbols.down } else { " " };
-                drop_print!(gctx, "{}   ", c);
+                drop_print!(ws.gctx(), "{}   ", c);
             }
 
-            drop_println!(gctx, "{}", name);
+            drop_println!(ws.gctx(), "{}", name);
         }
     }
 
+    let (max_display_depth, filter_non_workspace_member) = match display_depth {
+        DisplayDepth::MaxDisplayDepth(max) => (max, false),
+        DisplayDepth::Workspace => (u32::MAX, true),
+    };
+
     // Current level exceeds maximum display depth. Skip.
     if levels_continue.len() + 1 > max_display_depth as usize {
         return;
@@ -389,6 +422,9 @@ fn print_dependencies<'a>(
             // Filter out packages to prune.
             match graph.node(**dep) {
                 Node::Package { package_id, .. } => {
+                    if filter_non_workspace_member && !ws.is_member_id(*package_id) {
+                        return false;
+                    }
                     !pkgs_to_prune.iter().any(|spec| spec.matches(*package_id))
                 }
                 _ => true,
@@ -399,7 +435,7 @@ fn print_dependencies<'a>(
     while let Some(dependency) = it.next() {
         levels_continue.push(it.peek().is_some());
         print_node(
-            gctx,
+            ws,
             graph,
             *dependency,
             format,
@@ -407,7 +443,7 @@ fn print_dependencies<'a>(
             pkgs_to_prune,
             prefix,
             no_dedupe,
-            max_display_depth,
+            display_depth,
             visited_deps,
             levels_continue,
             print_stack,
diff --git a/src/doc/man/cargo-tree.md b/src/doc/man/cargo-tree.md
index ce99b761e..fc3521c99 100644
--- a/src/doc/man/cargo-tree.md
+++ b/src/doc/man/cargo-tree.md
@@ -96,6 +96,9 @@ Prune the given package from the display of the dependency tree.
 {{#option "`--depth` _depth_" }}
 Maximum display depth of the dependency tree. A depth of 1 displays the direct
 dependencies, for example.
+
+If the given value is `workspace`, only shows the dependencies that are member
+of the current workspace, instead.
 {{/option}}
 
 {{#option "`--no-dedupe`" }}
diff --git a/src/doc/man/generated_txt/cargo-tree.txt b/src/doc/man/generated_txt/cargo-tree.txt
index 1af730aff..0fc542f42 100644
--- a/src/doc/man/generated_txt/cargo-tree.txt
+++ b/src/doc/man/generated_txt/cargo-tree.txt
@@ -85,6 +85,9 @@ OPTIONS
            Maximum display depth of the dependency tree. A depth of 1 displays
            the direct dependencies, for example.
 
+           If the given value is workspace, only shows the dependencies that
+           are member of the current workspace, instead.
+
        --no-dedupe
            Do not de-duplicate repeated dependencies. Usually, when a package
            has already displayed its dependencies, further occurrences will not
diff --git a/src/doc/src/commands/cargo-tree.md b/src/doc/src/commands/cargo-tree.md
index 154939a28..9418eaa9d 100644
--- a/src/doc/src/commands/cargo-tree.md
+++ b/src/doc/src/commands/cargo-tree.md
@@ -91,7 +91,9 @@ subtree of the package given to <code>-p</code>.</dd>
 
 <dt class="option-term" id="option-cargo-tree---depth"><a class="option-anchor" href="#option-cargo-tree---depth"></a><code>--depth</code> <em>depth</em></dt>
 <dd class="option-desc">Maximum display depth of the dependency tree. A depth of 1 displays the direct
-dependencies, for example.</dd>
+dependencies, for example.</p>
+<p>If the given value is <code>workspace</code>, only shows the dependencies that are member
+of the current workspace, instead.</dd>
 
 
 <dt class="option-term" id="option-cargo-tree---no-dedupe"><a class="option-anchor" href="#option-cargo-tree---no-dedupe"></a><code>--no-dedupe</code></dt>
diff --git a/src/etc/man/cargo-tree.1 b/src/etc/man/cargo-tree.1
index c951460b0..25045b3f9 100644
--- a/src/etc/man/cargo-tree.1
+++ b/src/etc/man/cargo-tree.1
@@ -93,6 +93,9 @@ Prune the given package from the display of the dependency tree.
 .RS 4
 Maximum display depth of the dependency tree. A depth of 1 displays the direct
 dependencies, for example.
+.sp
+If the given value is \fBworkspace\fR, only shows the dependencies that are member
+of the current workspace, instead.
 .RE
 .sp
 \fB\-\-no\-dedupe\fR


Test patch:
diff --git a/tests/testsuite/tree.rs b/tests/testsuite/tree.rs
index 60374bb59..762cc4d96 100644
--- a/tests/testsuite/tree.rs
+++ b/tests/testsuite/tree.rs
@@ -1846,6 +1846,61 @@ c v1.0.0
         .run();
 }
 
+#[cargo_test]
+fn depth_workspace() {
+    Package::new("somedep", "1.0.0").publish();
+    Package::new("otherdep", "1.0.0").publish();
+    let p = project()
+        .file(
+            "Cargo.toml",
+            r#"
+            [workspace]
+            members = ["a", "b", "c"]
+            "#,
+        )
+        .file("a/Cargo.toml", &basic_manifest("a", "1.0.0"))
+        .file("a/src/lib.rs", "")
+        .file(
+            "b/Cargo.toml",
+            r#"
+            [package]
+            name = "b"
+            version = "0.1.0"
+
+            [dependencies]
+            c = { path = "../c" }
+            somedep = "1"
+            "#,
+        )
+        .file("b/src/lib.rs", "")
+        .file(
+            "c/Cargo.toml",
+            r#"
+            [package]
+            name = "c"
+            version = "0.1.0"
+
+            [dependencies]
+            somedep = "1"
+            otherdep = "1"
+            "#,
+        )
+        .file("c/src/lib.rs", "")
+        .build();
+
+    p.cargo("tree --depth workspace")
+        .with_stdout_data(str![[r#"
+a v1.0.0 ([ROOT]/foo/a)
+
+b v0.1.0 ([ROOT]/foo/b)
+└── c v0.1.0 ([ROOT]/foo/c)
+
+c v0.1.0 ([ROOT]/foo/c) (*)
+
+"#]])
+        .run();
+}
+
 #[cargo_test]
 fn prune() {
     let p = make_simple_proj();