diff --git a/module/move/willbe/src/command/list.rs b/module/move/willbe/src/command/list.rs index b18b85a4ba..6a209d0f5b 100644 --- a/module/move/willbe/src/command/list.rs +++ b/module/move/willbe/src/command/list.rs @@ -17,6 +17,31 @@ mod private use path::AbsolutePath; use endpoint::{ list as l, list::{ ListFormat, ListArgs } }; + use former::Former; + + #[ derive( Former ) ] + struct ListProperties + { + #[ default( ListFormat::Tree ) ] + format : ListFormat, + + #[ default( false ) ] + with_version : bool, + #[ default( false ) ] + with_path : bool, + + #[ default( true ) ] + with_local : bool, + #[ default( false ) ] + with_remote : bool, + + #[ default( true ) ] + with_primary : bool, + #[ default( false ) ] + with_dev : bool, + #[ default( false ) ] + with_build : bool, + } /// /// List workspace packages. @@ -27,17 +52,14 @@ mod private let path_to_workspace : PathBuf = args.get_owned( 0 ).unwrap_or( std::env::current_dir().context( "Workspace list command without subject" )? ); let path_to_workspace = AbsolutePath::try_from( path_to_workspace )?; - let format = properties.get_owned( "format" ).map( ListFormat::from_str ).transpose()?.unwrap_or_default(); - - let with_local = properties.get_owned( "with_local" ).unwrap_or( true ); - let with_remote = properties.get_owned( "with_remote" ).unwrap_or( false ); - - let with_primary = properties.get_owned( "with_primary" ).unwrap_or( true ); - let with_dev = properties.get_owned( "with_dev" ).unwrap_or( false ); - let with_build = properties.get_owned( "with_build" ).unwrap_or( false ); + let ListProperties { format, with_version, with_path, with_local, with_remote, with_primary, with_dev, with_build } = ListProperties::try_from( properties )?; let crate_dir = CrateDir::try_from( path_to_workspace )?; + let mut additional_info = HashSet::new(); + if with_version { additional_info.insert( l::PackageAdditionalInfo::Version ); } + if with_path { additional_info.insert( l::PackageAdditionalInfo::Path ); } + let mut sources = HashSet::new(); if with_local { sources.insert( l::DependencySource::Local ); } if with_remote { sources.insert( l::DependencySource::Remote ); } @@ -50,13 +72,14 @@ mod private let args = ListArgs::former() .path_to_manifest( crate_dir ) .format( format ) + .info( additional_info ) .dependency_sources( sources ) .dependency_categories( categories ) .form(); - match endpoint::listv2( args ) + match endpoint::list( args ) { - core::result::Result::Ok( report ) => + Ok( report ) => { println!( "{report}" ); } @@ -70,6 +93,27 @@ mod private Ok( () ) } + + impl TryFrom< Props > for ListProperties + { + type Error = wtools::error::for_app::Error; + fn try_from( value : Props ) -> Result< Self, Self::Error > + { + let mut this = Self::former(); + + this = if let Some( v ) = value.get_owned( "format" ).map( ListFormat::from_str ) { this.format( v? ) } else { this }; + this = if let Some( v ) = value.get_owned( "with_version" ) { this.with_version::< bool >( v ) } else { this }; + this = if let Some( v ) = value.get_owned( "with_path" ) { this.with_path::< bool >( v ) } else { this }; + this = if let Some( v ) = value.get_owned( "with_local" ) { this.with_local::< bool >( v ) } else { this }; + this = if let Some( v ) = value.get_owned( "with_remote" ) { this.with_remote::< bool >( v ) } else { this }; + this = if let Some( v ) = value.get_owned( "with_primary" ) { this.with_primary::< bool >( v ) } else { this }; + this = if let Some( v ) = value.get_owned( "with_dev" ) { this.with_dev::< bool >( v ) } else { this }; + this = if let Some( v ) = value.get_owned( "with_build" ) { this.with_build::< bool >( v ) } else { this }; + + Ok( this.form() ) + } + } + } // diff --git a/module/move/willbe/src/command/mod.rs b/module/move/willbe/src/command/mod.rs index 009fd38cc3..804116559c 100644 --- a/module/move/willbe/src/command/mod.rs +++ b/module/move/willbe/src/command/mod.rs @@ -28,6 +28,8 @@ pub( crate ) mod private .phrase( "list" ) .subject( "The command will generate a list of packages based on a path that must containing a `Cargo.toml` file. If no path is provided, the current directory is used.", Type::Path, true ) .property( "format", "Adjusts the output format - 'topsort' for a topologically sorted list or 'tree' for a structure of independent crates trees. The default is `tree`.", Type::String, true ) + .property( "with_version", "`true` to include the versions of the packages in the output. Defaults to `false`.", Type::Bool, true ) + .property( "with_path", "`true` to include the paths of the packages in the output. Defaults to `false`.", Type::Bool, true ) .property( "with_primary", "`true` to include primary packages in the output, `false` otherwise. Defaults to `true`.", Type::Bool, true ) .property( "with_dev", "`true` to include development packages in the output, `false` otherwise. Defaults to `false`.", Type::Bool, true ) .property( "with_build", "`true` to include build packages in the output, `false` otherwise. Defaults to `false`.", Type::Bool, true ) diff --git a/module/move/willbe/src/endpoint/list.rs b/module/move/willbe/src/endpoint/list.rs index 905b3ccdc8..6b98f5280c 100644 --- a/module/move/willbe/src/endpoint/list.rs +++ b/module/move/willbe/src/endpoint/list.rs @@ -8,17 +8,18 @@ mod private path::PathBuf, collections::HashSet, }; + use std::collections::HashMap; use petgraph:: { prelude::*, - algo::{ toposort, has_path_connecting }, + algo::toposort, visit::Topo, }; use std::str::FromStr; use packages::FilterMapOptions; use wtools::error:: { - for_app::{ Error, Context, format_err }, + for_app::{ Error, Context }, err }; use cargo_metadata:: @@ -127,80 +128,14 @@ mod private } } - /// Output of the `list` endpoint - #[ derive( Debug, Default, Clone ) ] - pub enum ListReport - { - /// With tree format. - Tree - { - /// Dependencies graph. - graph : Graph< String, String >, - /// Packages indexes to display. - names : Vec< petgraph::stable_graph::NodeIndex >, - }, - /// With topologically sorted list. - List( Vec< String > ), - /// Nothing to show. - #[ default ] - Empty - } - - /// Wrapper to redirect output from `ptree` graph to `fmt::Write` - pub struct Io2FmtWrite< 'a, W > - { - /// This struct provides a mutable reference to a writer and is used as a formatting writer. - pub f : &'a mut W, - } - - impl< T > std::fmt::Debug for Io2FmtWrite< '_, T > - { - fn fmt( &self, f : &mut Formatter< '_ > ) -> std::fmt::Result - { - f.debug_struct( std::any::type_name::< Self >() ).finish() - } - } - - impl< W : std::fmt::Write > std::io::Write for Io2FmtWrite< '_, W > - { - fn write( &mut self, buf : &[ u8 ] ) -> std::io::Result< usize > - { - use std::io::ErrorKind; - - let size = buf.len(); - - self.f.write_str - ( - std::str::from_utf8( buf ) - .map_err( | _ | std::io::Error::new( ErrorKind::InvalidData, "Allow only valid UTF-8 string" ) )? - ) - .map_err( | e | std::io::Error::new( ErrorKind::Other, e ) )?; - - Ok( size ) - } - - fn flush( &mut self ) -> std::io::Result< () > - { - Ok( () ) - } - } - - impl std::fmt::Display for ListReport + /// Additional information to include in a package report. + #[ derive( Debug, Copy, Clone, Hash, Eq, PartialEq ) ] + pub enum PackageAdditionalInfo { - fn fmt( &self, f : &mut Formatter< '_ > ) -> std::fmt::Result - { - match self - { - ListReport::Tree { graph, names } => for n in names - { - ptree::graph::write_graph_with( &graph, *n, Io2FmtWrite { f }, &ptree::PrintConfig::from_env() ).unwrap(); - }, - ListReport::List ( list ) => for ( i ,e ) in list.iter().enumerate() { writeln!( f, "{i}) {e}" )? }, - _ => {}, - } - - Ok( () ) - } + /// Include the version of the package, if possible. + Version, + /// Include the path to the package, if it exists. + Path, } /// A struct representing the arguments for listing crates. @@ -216,6 +151,7 @@ mod private { path_to_manifest : CrateDir, format : ListFormat, + info: HashSet< PackageAdditionalInfo >, dependency_sources: HashSet< DependencySource >, dependency_categories: HashSet< DependencyCategory >, } @@ -282,22 +218,28 @@ mod private } } + /// Represents the different report formats for the `list` endpoint. #[ derive( Debug, Default, Clone ) ] - pub enum ListReportV2 + pub enum ListReport { + /// Represents a tree-like report format. Tree( Vec< ListNodeReport > ), + /// Represents a standard list report format in topological order. + List( Vec< String > ), + /// Represents an empty report format. #[ default ] Empty, } - impl std::fmt::Display for ListReportV2 + impl std::fmt::Display for ListReport { fn fmt( &self, f : &mut Formatter< '_ > ) -> std::fmt::Result { match self { Self::Tree( v ) => write!( f, "{}", v.iter().map( | l | l.to_string() ).collect::< Vec< _ > >().join( "\n" ) ), - Self::Empty => write!( f, "" ), + Self::List( v ) => write!( f, "{}", v.iter().enumerate().map( |( i, v )| format!( "[{i}] {v}" ) ).collect::< Vec< _ > >().join( "\n" ) ), + Self::Empty => write!( f, "Nothing" ), } } } @@ -337,8 +279,8 @@ mod private let mut dep_rep = ListNodeReport { name : dep.name.clone(), - version : Some( dep.req.to_string() ), - path : dep.path.as_ref().map( | p | p.clone().into_std_path_buf() ), + version : if args.info.contains( &PackageAdditionalInfo::Version ) { Some( dep.req.to_string() ) } else { None }, + path : if args.info.contains( &PackageAdditionalInfo::Path ) { dep.path.as_ref().map( | p | p.clone().into_std_path_buf() ) } else { None }, normal_dependencies : vec![], dev_dependencies : vec![], build_dependencies : vec![], @@ -379,24 +321,33 @@ mod private } } - /// - - pub fn listv2( args : ListArgs ) -> Result< ListReportV2, ( ListReportV2, Error ) > + /// Retrieve a list of packages based on the given arguments. + /// + /// # Arguments + /// + /// - `args`: ListArgs - The arguments for listing packages. + /// + /// # Returns + /// + /// - `Result` - A result containing the list report if successful, + /// or a tuple containing the list report and error if not successful. + pub fn list( args : ListArgs ) -> Result< ListReport, ( ListReport, Error ) > { - let mut report = ListReportV2::default(); + let mut report = ListReport::default(); let manifest = manifest::open( args.path_to_manifest.absolute_path() ).context( "List of packages by specified manifest path" ).err_with( report.clone() )?; let metadata = Workspace::with_crate_dir( manifest.crate_dir() ).err_with( report.clone() )?; let is_package = manifest.package_is().context( "try to identify manifest type" ).err_with( report.clone() )?; - let tree_package_report = | path : AbsolutePath, report : &mut ListReportV2, visited : &mut HashSet< String > | + let tree_package_report = | path : AbsolutePath, report : &mut ListReport, visited : &mut HashSet< String > | { let package = metadata.package_find_by_manifest( path ).unwrap(); let mut package_report = ListNodeReport { name: package.name.clone(), - version: Some( package.version.to_string() ), - path: Some( package.manifest_path.clone().into_std_path_buf() ), + version : if args.info.contains( &PackageAdditionalInfo::Version ) { Some( package.version.to_string() ) } else { None }, + path : if args.info.contains( &PackageAdditionalInfo::Path ) { Some( package.manifest_path.clone().into_std_path_buf() ) } else { None }, normal_dependencies: vec![], dev_dependencies: vec![], build_dependencies: vec![], @@ -406,8 +357,9 @@ mod private *report = match report { - ListReportV2::Tree( ref mut v ) => ListReportV2::Tree( { v.extend([ package_report ]); v.clone() } ), - ListReportV2::Empty => ListReportV2::Tree( vec![ package_report ] ), + ListReport::Tree(ref mut v ) => ListReport::Tree( { v.extend([ package_report ]); v.clone() } ), + ListReport::Empty => ListReport::Tree( vec![ package_report ] ), + ListReport::List(_ ) => unreachable!(), }; }; match args.format @@ -426,125 +378,114 @@ mod private tree_package_report( package.manifest_path.as_path().try_into().unwrap(), &mut report, &mut visited ) } } - _ => { unimplemented!() } - } - - Ok( report ) - } - - /// - /// List workspace packages. - /// - - pub fn list( args : ListArgs ) -> Result< ListReport, ( ListReport, Error ) > - { - let mut report = ListReport::default(); - - let manifest = manifest::open( args.path_to_manifest.absolute_path() ).context( "List of packages by specified manifest path" ).map_err( | e | ( report.clone(), e.into() ) )?; - let mut metadata = Workspace::with_crate_dir( manifest.crate_dir() ).map_err( | err | ( report.clone(), format_err!( err ) ) )?; - - let root_crate = manifest - .manifest_data - .as_ref() - .and_then( | m | m.get( "package" ) ) - .map( | m | m[ "name" ].to_string().trim().replace( '\"', "" ) ) - .unwrap_or_default(); - - // let packages_map = metadata.packages.iter().map( | p | ( p.name.clone(), p ) ).collect::< HashMap< _, _ > >(); - - let dep_filter = move | _p: &Package, d: &Dependency | - { - ( - args.dependency_categories.contains( &DependencyCategory::Primary ) && d.kind == DependencyKind::Normal - || args.dependency_categories.contains( &DependencyCategory::Dev ) && d.kind == DependencyKind::Development - || args.dependency_categories.contains( &DependencyCategory::Build ) && d.kind == DependencyKind::Build - ) - && - ( - args.dependency_sources.contains( &DependencySource::Remote ) && d.path.is_none() - || args.dependency_sources.contains( &DependencySource::Local ) && d.path.is_some() - ) - }; + ListFormat::Topological => + { + let root_crate = manifest + .manifest_data + .as_ref() + .and_then( | m | m.get( "package" ) ) + .map( | m | m[ "name" ].to_string().trim().replace( '\"', "" ) ) + .unwrap_or_default(); + + let dep_filter = move | _p: &Package, d: &Dependency | + { + ( + args.dependency_categories.contains( &DependencyCategory::Primary ) && d.kind == DependencyKind::Normal + || args.dependency_categories.contains( &DependencyCategory::Dev ) && d.kind == DependencyKind::Development + || args.dependency_categories.contains( &DependencyCategory::Build ) && d.kind == DependencyKind::Build + ) + && + ( + args.dependency_sources.contains( &DependencySource::Remote ) && d.path.is_none() + || args.dependency_sources.contains( &DependencySource::Local ) && d.path.is_some() + ) + }; - let packages_map = packages::filter - ( - &metadata - .load() - .map_err( | err | ( report.clone(), format_err!( err ) ) )? - .packages_get() - .map_err( | err | ( report.clone(), format_err!( err ) ) )?, - FilterMapOptions{ dependency_filter: Some( Box::new( dep_filter ) ), ..Default::default() } - ); + let packages = metadata.packages_get().context( "workspace packages" ).err_with( report.clone() )?; + let packages_map = packages::filter + ( + packages, + FilterMapOptions{ dependency_filter: Some( Box::new( dep_filter ) ), ..Default::default() } + ); - let graph = graph::construct( &packages_map ); + let graph = graph::construct( &packages_map ); - let sorted = toposort( &graph, None ).map_err( | e | { use std::ops::Index; ( report.clone(), err!( "Failed to process toposort for package: {:?}", graph.index( e.node_id() ) ) ) } )?; + let sorted = toposort( &graph, None ).map_err( | e | { use std::ops::Index; ( report.clone(), err!( "Failed to process toposort for package: {:?}", graph.index( e.node_id() ) ) ) } )?; + let packages_info = packages.iter().map( | p | ( p.name.clone(), p ) ).collect::< HashMap< _, _ > >(); - match args.format - { - // all crates - ListFormat::Tree if root_crate.is_empty() => - { - let mut names = vec![ sorted[ 0 ] ]; - for node in sorted.iter().skip( 1 ) + if root_crate.is_empty() { - if names.iter().all( | name | !has_path_connecting( &graph, *name, *node, None ) ) && !names.contains( node ) + let names = sorted + .iter() + .rev() + .map( | dep_idx | graph.node_weight( *dep_idx ).unwrap().to_string() ) + .map + ( + | mut name | + { + if let Some( p ) = packages_info.get( &name ) + { + if args.info.contains( &PackageAdditionalInfo::Version ) + { + name.push_str( " " ); + name.push_str( &p.version.to_string() ); + } + if args.info.contains( &PackageAdditionalInfo::Path ) + { + name.push_str( " " ); + name.push_str( &p.manifest_path.to_string() ); + } + } + name + } + ) + .collect::< Vec< String > >(); + + report = ListReport::List( names ); + } + else + { + let node = graph.node_indices().find( | n | graph.node_weight( *n ).unwrap() == &&root_crate ).unwrap(); + let mut dfs = Dfs::new( &graph, node ); + let mut subgraph = Graph::new(); + let mut node_map = std::collections::HashMap::new(); + while let Some( n )= dfs.next( &graph ) { - names.push( *node ); + node_map.insert( n, subgraph.add_node( graph[ n ] ) ); } - } - report = ListReport::Tree { graph : graph.map( | _, &n | String::from( n ), | _, &e | String::from( e ) ), names }; - }, - // specific crate as root - ListFormat::Tree => - { - let names = sorted - .iter() - .filter_map( | idx | if graph.node_weight( *idx ).unwrap() == &&root_crate { Some( *idx ) } else { None } ) - .collect::< Vec< _ > >(); - report = ListReport::Tree { graph : graph.map( | _, &n | String::from( n ), | _, &e | String::from( e ) ), names }; - } - // all crates - ListFormat::Topological if root_crate.is_empty() => - { - let names = sorted - .iter() - .rev() - .map( | dep_idx | graph.node_weight( *dep_idx ).unwrap().to_string() ) - .collect::< Vec< String > >(); - - report = ListReport::List( names ); - }, - // specific crate as root - ListFormat::Topological => - { - let node = graph.node_indices().find( | n | graph.node_weight( *n ).unwrap() == &&root_crate ).unwrap(); - let mut dfs = Dfs::new( &graph, node ); - let mut subgraph = Graph::new(); - let mut node_map = std::collections::HashMap::new(); - while let Some( n )= dfs.next( &graph ) - { - node_map.insert( n, subgraph.add_node( graph[ n ] ) ); - } + for e in graph.edge_references() + { + if let ( Some( &s ), Some( &t ) ) = ( node_map.get( &e.source() ), node_map.get( &e.target() ) ) + { + subgraph.add_edge( s, t, () ); + } + } - for e in graph.edge_references() - { - if let ( Some( &s ), Some( &t ) ) = ( node_map.get( &e.source() ), node_map.get( &e.target() ) ) + let mut topo = Topo::new( &subgraph ); + let mut names = Vec::new(); + while let Some( n ) = topo.next( &subgraph ) { - subgraph.add_edge( s, t, () ); + let mut name = subgraph[ n ].clone(); + if let Some( p ) = packages_info.get( &name ) + { + if args.info.contains( &PackageAdditionalInfo::Version ) + { + name.push_str( " " ); + name.push_str( &p.version.to_string() ); + } + if args.info.contains( &PackageAdditionalInfo::Path ) + { + name.push_str( " " ); + name.push_str( &p.manifest_path.to_string() ); + } + } + names.push( name ); } - } + names.reverse(); - let mut topo = Topo::new( &subgraph ); - let mut names = Vec::new(); - while let Some( n ) = topo.next( &subgraph ) - { - names.push( subgraph[ n ].clone() ); + report = ListReport::List( names ); } - names.reverse(); - - report = ListReport::List( names ); } } @@ -558,6 +499,8 @@ crate::mod_interface! { /// Arguments for `list` endpoint. protected use ListArgs; + /// Additional information to include in a package report. + protected use PackageAdditionalInfo; /// Represents where a dependency located. protected use DependencySource; /// Represents the category of a dependency. @@ -570,7 +513,4 @@ crate::mod_interface! protected use ListReport; /// List packages in workspace. orphan use list; - orphan use listv2; - /// Wrapper to redirect output from `io::Write` to `fmt::Write` - protected use Io2FmtWrite; }