From f6a47dbdbd12e16a9b6139d02e8460a93f7f83b4 Mon Sep 17 00:00:00 2001 From: Beast-ofcourse Date: Wed, 8 Jul 2026 11:39:16 +0530 Subject: [PATCH 1/3] fix(uv): tree --invert now handles leaf cycles without silently dropping the graph --- crates/uv-resolver/src/lock/tree.rs | 34 ++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/crates/uv-resolver/src/lock/tree.rs b/crates/uv-resolver/src/lock/tree.rs index e9bacb5726f2f..7f69140a753fe 100644 --- a/crates/uv-resolver/src/lock/tree.rs +++ b/crates/uv-resolver/src/lock/tree.rs @@ -5,6 +5,7 @@ use std::fmt::Write; use either::Either; use itertools::Itertools; use owo_colors::OwoColorize; +use petgraph::algo::tarjan_scc; use petgraph::graph::{EdgeIndex, NodeIndex}; use petgraph::prelude::EdgeRef; use petgraph::{Direction, Graph}; @@ -436,8 +437,8 @@ impl<'env> TreeDisplay<'env> { } else { let mut roots = if invert { // For inverted trees, find leaf packages (nodes with no incoming - // edges). - graph + // edges after reversal). + let mut roots: Vec<_> = graph .node_indices() .filter(|index| { graph @@ -445,7 +446,34 @@ impl<'env> TreeDisplay<'env> { .next() .is_none() }) - .collect::>() + .collect(); + + // If no node has zero incoming edges (e.g., a leaf cycle), fall + // back to treating each root SCC as a root. A root SCC is one + // whose nodes have no incoming edges from outside the SCC. + if roots.is_empty() { + let sccs = tarjan_scc(&graph); + for scc in &sccs { + let has_external_incoming = scc.iter().any(|node| { + graph + .edges_directed(*node, Direction::Incoming) + .any(|edge| !scc.contains(&edge.source())) + }); + if !has_external_incoming { + // Pick the best node in the SCC as root: prefer + // packages over Root nodes. + let root = scc + .iter() + .find(|n| matches!(graph[**n], Node::Package(_))) + .or_else(|| scc.first()); + if let Some(root) = root { + roots.push(*root); + } + } + } + } + + roots } else { // For non-inverted trees, use the root node directly. graph From d856a5ff6b168852a91ba49be17839a4640797fb Mon Sep 17 00:00:00 2001 From: Beast-ofcourse Date: Wed, 8 Jul 2026 13:00:44 +0530 Subject: [PATCH 2/3] test: add invert_leaf_cycle integration test for tree --invert with leaf cycles --- crates/uv/tests/project/tree.rs | 89 +++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/crates/uv/tests/project/tree.rs b/crates/uv/tests/project/tree.rs index cfd4fb3726570..775832fdf17fb 100644 --- a/crates/uv/tests/project/tree.rs +++ b/crates/uv/tests/project/tree.rs @@ -2998,3 +2998,92 @@ fn workspace_circular_dependencies() -> Result<()> { Ok(()) } + +#[test] +fn invert_leaf_cycle() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + // Create a lockfile with a leaf cycle: bar ↔ baz form a cycle where no + // node in the reversed graph has zero incoming edges. Without the SCC + // fallback, `--invert` would produce no output. + context + .temp_dir + .child("pyproject.toml") + .write_str(indoc! {r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.12" + dependencies = ["bar==1.0.0"] + "#})?; + + context + .temp_dir + .child("uv.lock") + .write_str(indoc! {r#" + version = 1 + revision = 3 + requires-python = ">=3.12" + + [[package]] + name = "foo" + version = "1.0.0" + source = { virtual = "." } + dependencies = [ + { name = "bar" }, + ] + + [[package]] + name = "bar" + version = "1.0.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "baz" }, + ] + + [[package]] + name = "baz" + version = "1.0.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "bar" }, + ] + "#})?; + + // Normal tree — no issue; the cycle is displayed as usual. + uv_snapshot!(context.filters(), context.tree().arg("--frozen"), @r" + success: true + exit_code: 0 + ----- stdout ----- + foo v1.0.0 + └── bar v1.0.0 + └── baz v1.0.0 + └── bar v1.0.0 (*) + (*) Package tree already displayed + + ----- stderr ----- + + " + ); + + // Inverted tree — without the SCC fallback every node in the reversed + // graph has incoming edges, yielding an empty root set and no output. + // With the fix, the SCC {bar, baz} is detected as a root SCC. + uv_snapshot!(context.filters(), context.tree().arg("--frozen").arg("--invert"), @r" + success: true + exit_code: 0 + ----- stdout ----- + bar v1.0.0 + ├── baz v1.0.0 + │ └── bar v1.0.0 (*) + └── foo v1.0.0 + + (*) Package tree already displayed + + ----- stderr ----- + + " + ); + + Ok(()) +} From a64c71e6b54606cd26b93cb1f833083696dab00a Mon Sep 17 00:00:00 2001 From: Beast-ofcourse Date: Wed, 8 Jul 2026 15:09:44 +0530 Subject: [PATCH 3/3] fmt: collapse .child().write_str() chain in invert_leaf_cycle test --- crates/uv/tests/project/tree.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/uv/tests/project/tree.rs b/crates/uv/tests/project/tree.rs index 775832fdf17fb..f3a4dc8d79772 100644 --- a/crates/uv/tests/project/tree.rs +++ b/crates/uv/tests/project/tree.rs @@ -3017,10 +3017,7 @@ fn invert_leaf_cycle() -> Result<()> { dependencies = ["bar==1.0.0"] "#})?; - context - .temp_dir - .child("uv.lock") - .write_str(indoc! {r#" + context.temp_dir.child("uv.lock").write_str(indoc! {r#" version = 1 revision = 3 requires-python = ">=3.12"