Add uv tree --outdated (#8893)

## Summary

Similar to `pip list --outdated`, but for `uv tree`.

## Test Plan

Looks like:

```
foo v0.1.0
└── flask v2.0.0 (latest: v3.0.3)
    ├── click v8.1.7
    ├── itsdangerous v2.2.0
    ├── jinja2 v3.1.4
    │   └── markupsafe v3.0.2
    └── werkzeug v3.1.2
        └── markupsafe v3.0.2
```

With `(latest: v3.0.3)` in bold cyan.
This commit is contained in:
Charlie Marsh 2024-11-07 15:10:46 -05:00 committed by GitHub
parent 88331e756e
commit 29e1b15473
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 328 additions and 118 deletions

View file

@ -4,8 +4,8 @@ pub use exclude_newer::ExcludeNewer;
pub use exclusions::Exclusions;
pub use flat_index::{FlatDistributions, FlatIndex};
pub use lock::{
InstallTarget, Lock, LockError, LockVersion, RequirementsTxtExport, ResolverManifest,
SatisfiesResult, TreeDisplay, VERSION,
InstallTarget, Lock, LockError, LockVersion, PackageMap, RequirementsTxtExport,
ResolverManifest, SatisfiesResult, TreeDisplay, VERSION,
};
pub use manifest::Manifest;
pub use options::{Flexibility, Options, OptionsBuilder};

View file

@ -0,0 +1,37 @@
use rustc_hash::FxHashMap;
use crate::lock::{Package, PackageId};
/// A map from package to values, indexed by [`PackageId`].
#[derive(Debug, Clone)]
pub struct PackageMap<T>(FxHashMap<PackageId, T>);
impl<T> Default for PackageMap<T> {
fn default() -> Self {
Self(FxHashMap::default())
}
}
impl<T> PackageMap<T> {
/// Get a value by [`PackageId`].
pub(crate) fn get(&self, package_id: &PackageId) -> Option<&T> {
self.0.get(package_id)
}
}
impl<T> FromIterator<(Package, T)> for PackageMap<T> {
fn from_iter<I: IntoIterator<Item = (Package, T)>>(iter: I) -> Self {
Self(
iter.into_iter()
.map(|(package, value)| (package.id, value))
.collect(),
)
}
}
impl<T> Extend<(Package, T)> for PackageMap<T> {
fn extend<I: IntoIterator<Item = (Package, T)>>(&mut self, iter: I) {
self.0
.extend(iter.into_iter().map(|(package, value)| (package.id, value)));
}
}

View file

@ -14,6 +14,7 @@ use std::sync::{Arc, LazyLock};
use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value};
use url::Url;
pub use crate::lock::map::PackageMap;
pub use crate::lock::requirements_txt::RequirementsTxtExport;
pub use crate::lock::target::InstallTarget;
pub use crate::lock::tree::TreeDisplay;
@ -47,6 +48,7 @@ use uv_types::{BuildContext, HashStrategy};
use uv_workspace::dependency_groups::DependencyGroupError;
use uv_workspace::Workspace;
mod map;
mod requirements_txt;
mod target;
mod tree;
@ -2206,6 +2208,24 @@ impl Package {
self.fork_markers.as_slice()
}
/// Returns the [`IndexUrl`] for the package, if it is a registry source.
pub fn index(&self, root: &Path) -> Result<Option<IndexUrl>, LockError> {
match &self.id.source {
Source::Registry(RegistrySource::Url(url)) => {
let index = IndexUrl::from(VerbatimUrl::from_url(url.to_url()));
Ok(Some(index))
}
Source::Registry(RegistrySource::Path(path)) => {
let index = IndexUrl::from(
VerbatimUrl::from_absolute_path(root.join(path))
.map_err(LockErrorKind::RegistryVerbatimUrl)?,
);
Ok(Some(index))
}
_ => Ok(None),
}
}
/// Returns all the hashes associated with this [`Package`].
fn hashes(&self) -> Vec<HashDigest> {
let mut hashes = Vec::new();

View file

@ -2,6 +2,7 @@ use std::borrow::Cow;
use std::collections::VecDeque;
use itertools::Itertools;
use owo_colors::OwoColorize;
use petgraph::graph::{EdgeIndex, NodeIndex};
use petgraph::prelude::EdgeRef;
use petgraph::Direction;
@ -9,10 +10,11 @@ use rustc_hash::{FxHashMap, FxHashSet};
use uv_configuration::DevGroupsManifest;
use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep440::Version;
use uv_pypi_types::ResolverMarkerEnvironment;
use crate::lock::{Dependency, PackageId};
use crate::Lock;
use crate::{Lock, PackageMap};
#[derive(Debug)]
pub struct TreeDisplay<'env> {
@ -20,6 +22,8 @@ pub struct TreeDisplay<'env> {
graph: petgraph::graph::Graph<&'env PackageId, Edge<'env>, petgraph::Directed>,
/// The packages considered as roots of the dependency tree.
roots: Vec<NodeIndex>,
/// The latest known version of each package.
latest: &'env PackageMap<Version>,
/// Maximum display depth of the dependency tree.
depth: usize,
/// Whether to de-duplicate the displayed dependencies.
@ -31,6 +35,7 @@ impl<'env> TreeDisplay<'env> {
pub fn new(
lock: &'env Lock,
markers: Option<&'env ResolverMarkerEnvironment>,
latest: &'env PackageMap<Version>,
depth: usize,
prune: &[PackageName],
packages: &[PackageName],
@ -242,6 +247,7 @@ impl<'env> TreeDisplay<'env> {
Self {
graph,
roots,
latest,
depth,
no_dedupe,
}
@ -306,6 +312,13 @@ impl<'env> TreeDisplay<'env> {
}
}
// Incorporate the latest version of the package, if known.
let line = if let Some(version) = self.latest.get(package_id) {
format!("{line} {}", format!("(latest: v{version})").bold().cyan())
} else {
line
};
let mut dependencies = self
.graph
.edges_directed(cursor.node(), Direction::Outgoing)