From e554e7033320456762a82b8276e0137592d57dcb Mon Sep 17 00:00:00 2001 From: Toby Vincent Date: Fri, 5 May 2023 18:42:37 -0500 Subject: refactor: rewrite project layout --- src/config.rs | 33 ++++++++----- src/git.rs | 29 ++++++++---- src/lib.rs | 5 +- src/main.rs | 46 +++--------------- src/parser.rs | 45 +++++++++--------- src/path.rs | 16 +++---- src/project.rs | 146 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- src/search.rs | 92 ++++++++++++++++++++++-------------- 8 files changed, 268 insertions(+), 144 deletions(-) (limited to 'src') diff --git a/src/config.rs b/src/config.rs index b87f88d..aa9a6c4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,24 +6,29 @@ use tracing::{metadata::LevelFilter, Level}; #[derive(Debug, Clone, Default, Parser)] #[command(author, version, about)] pub struct Config { - /// Directories to search. - /// - /// Directories are searched recursively based on `--max-depth`. - pub paths: Vec, + #[command(flatten)] + pub search: Search, - /// (UNIMPLEMENTED) Additional directories to add to the sorted output. + #[command(flatten)] + pub parsers: Projects, + + /// Add arbitrary directories + /// + /// Directories added by this flag are still filtered and sorted based on supplied options. #[arg(long = "project", short = 'P')] pub projects: Vec, - #[command(flatten)] - pub search: Search, - #[command(flatten)] pub verbosity: Verbosity, } #[derive(Debug, Default, Clone, Args)] pub struct Search { + /// Directories to search. + /// + /// Directories are searched recursively based on `--max-depth`. + pub paths: Vec, + /// Recurse into hidden directories. /// /// Traverse into hidden directories while searching. A directory is considered hidden @@ -36,24 +41,26 @@ pub struct Search { /// MAX_DEPTH of 0 will only return the supplied PATHS. #[arg(short = 'd', long, default_value = "1")] pub max_depth: Option, - - #[command(flatten)] - pub parsers: Parsers, } #[derive(Debug, Default, Clone, Args)] -pub struct Parsers { +pub struct Projects { /// Match all child directories. + /// + /// Uses the directory mtime as the timestamp. #[arg(short, long)] pub all: bool, /// Match directories containing . /// - /// PATTERN should be a path relative to the searched directory. + /// Uses the directory mtime as the timestamp. PATTERN should be a path relative to the + /// searched directory. #[arg(long, short)] pub pattern: Option, /// Match git repositories. + /// + /// Uses the most recient commit as the timestamp. #[cfg(feature = "git")] #[arg(long, short)] pub git: bool, diff --git a/src/git.rs b/src/git.rs index afa4a9a..3b791e0 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,4 +1,4 @@ -use git2::{BranchType, Repository}; +use git2::{BranchType, Error, Repository}; use std::{path::PathBuf, time::Duration}; use crate::{parser::Parser, project::Project}; @@ -7,32 +7,43 @@ use crate::{parser::Parser, project::Project}; pub struct Git; impl Parser for Git { + type Error = Error; + #[tracing::instrument] - fn parse(&self, path_buf: PathBuf) -> Result> { + fn parse(&self, path_buf: PathBuf) -> Result { Repository::open(&path_buf)?.parse(path_buf) } } impl Parser for Repository { - fn parse(&self, path_buf: PathBuf) -> Result> { - let mut branches = self.branches(Some(BranchType::Local))?; - let timestamp = branches - .try_fold(0, |latest, branch| -> std::result::Result { + type Error = Error; + + fn parse(&self, path_buf: PathBuf) -> Result { + let timestamp = self + .branches(Some(BranchType::Local))? + .map(|branch| -> Result<_, Error> { let (branch, _) = branch?; let name = branch .name()? - .ok_or_else(|| git2::Error::from_str("Failed to find branch"))?; + .ok_or_else(|| Error::from_str("Failed to get branch name"))?; self.revparse_single(name)? .peel_to_commit() - .map(|c| (c.time().seconds() as u64).max(latest)) + .map(|c| c.time().seconds() as u64) + }) + .inspect(|res| { + if let Err(err) = res { + tracing::warn!(%err, "Skipping errored branch") + } }) + .flatten() + .max() .map(Duration::from_secs) .unwrap_or_default(); Ok(Project { - worktree: path_buf, + path_buf, timestamp, }) } diff --git a/src/lib.rs b/src/lib.rs index 830c912..af1a2fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub use crate::config::Config; -pub use crate::search::SearchBuilder; +pub use crate::search::Search; +pub mod config; pub mod parser; pub mod project; pub mod search; @@ -8,5 +9,3 @@ pub mod search; #[cfg(feature = "git")] pub mod git; pub mod path; - -pub mod config; diff --git a/src/main.rs b/src/main.rs index 33b170d..bdc7998 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Parser; -use projectr::{config::Config, path::PathMatcher, project::Project, search::SearchBuilder}; +use projectr::{config::Config, project::Projects, Search}; fn main() -> Result<()> { let config = Config::parse(); @@ -11,47 +11,13 @@ fn main() -> Result<()> { .with_max_level(&config.verbosity) .init(); - let mut projects: Vec = build_search(&config) - .filter(|p| !config.paths.contains(&p.worktree)) - .collect(); + let mut projects = Projects::from(config.parsers); - projects.sort_unstable_by_key(|p| p.timestamp); + projects.extend(config.projects); - for project in projects { - println!("{}", project.worktree.to_string_lossy()) + if let Ok(search) = Search::try_from(config.search) { + projects.extend(search); } - Ok(()) -} - -fn build_search(config: &Config) -> impl Iterator { - let (init, paths) = config.paths.split_first().unwrap(); - let mut builder = SearchBuilder::new(init); - - for path in paths { - builder.add(path); - } - - for path in &config.projects { - builder.project(path); - } - - builder.max_depth(config.search.max_depth); - - builder.hidden(!config.search.hidden); - - if config.search.parsers.all { - builder.parser(PathMatcher::All); - } - - if let Some(pattern) = &config.search.parsers.pattern { - builder.parser(PathMatcher::Pattern(pattern.to_owned())); - } - - #[cfg(feature = "git")] - if config.search.parsers.git { - builder.parser(projectr::git::Git); - } - - builder.build() + Ok(projects.write(std::io::stdout())?) } diff --git a/src/parser.rs b/src/parser.rs index 1d3f159..2eec51c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,35 +1,36 @@ -use std::{fmt::Display, path::PathBuf}; - -use tracing::warn; +use std::path::PathBuf; use crate::project::Project; -#[derive(Debug)] -pub struct NotMatched; +pub trait Parser { + type Error: std::error::Error; -impl Display for NotMatched { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Failed to parse path") - } + fn parse(&self, path_buf: PathBuf) -> Result; } -impl std::error::Error for NotMatched {} +pub trait FilterMap { + fn filter_map(&self, path_buf: PathBuf) -> Option; +} -pub trait Parser { - fn parse(&self, path_buf: PathBuf) -> Result>; +impl FilterMap for T +where + T: Parser, +{ + fn filter_map(&self, path_buf: PathBuf) -> Option { + match self.parse(path_buf) { + Ok(p) => Some(p), + Err(err) => { + tracing::info!(%err, "Parser failed to parse project"); + None + } + } + } } -impl Parser for Vec> { - fn parse(&self, path_buf: PathBuf) -> Result> { +impl FilterMap for Vec> { + fn filter_map(&self, path_buf: PathBuf) -> Option { self.iter() - .map(|p| p.parse(path_buf.to_owned())) - .inspect(|res| { - if let Err(err) = res { - warn!(%err, "Parser failed to match"); - } - }) - .flatten() + .filter_map(|p| p.filter_map(path_buf.to_owned())) .reduce(|max, p| p.max(max)) - .ok_or(Box::new(NotMatched)) } } diff --git a/src/path.rs b/src/path.rs index 05a9caa..1000b04 100644 --- a/src/path.rs +++ b/src/path.rs @@ -9,14 +9,14 @@ pub enum PathMatcher { } impl Parser for PathMatcher { - #[tracing::instrument] - fn parse(&self, path_buf: PathBuf) -> Result> { - let project = match self { - PathMatcher::All => path_buf.try_into()?, - PathMatcher::Pattern(p) if path_buf.join(p).exists() => path_buf.try_into()?, - _ => return Err(Box::new(std::io::Error::from(ErrorKind::NotFound))), - }; + type Error = std::io::Error; - Ok(project) + #[tracing::instrument] + fn parse(&self, path_buf: PathBuf) -> Result { + match self { + PathMatcher::All => path_buf.try_into(), + PathMatcher::Pattern(p) if path_buf.join(p).exists() => path_buf.try_into(), + _ => Err(std::io::Error::from(ErrorKind::NotFound)), + } } } diff --git a/src/project.rs b/src/project.rs index b71a3b8..7351ae4 100644 --- a/src/project.rs +++ b/src/project.rs @@ -1,30 +1,149 @@ use std::{ - convert::Infallible, + collections::{hash_map::Entry, HashMap}, + fmt::Display, + io::{BufWriter, Write}, + ops::{Deref, DerefMut}, path::PathBuf, time::{Duration, SystemTime}, }; -pub trait Generator { - type Error: std::error::Error; +use tracing::trace; - fn generate(self) -> Result, Self::Error>; +use crate::{parser::FilterMap, path::PathMatcher}; + +#[derive(Default)] +pub struct Projects { + inner: HashMap, + filters: Vec>, +} + +impl Projects { + pub fn new() -> Self { + Self::default() + } + + pub fn add_filter(&mut self, filter: T) { + self.filters.push(Box::new(filter)) + } + + pub fn insert(&mut self, item: Project) { + let span = tracing::trace_span!("Entry", ?item); + let _guard = span.enter(); + + match self.inner.entry(item.path_buf) { + Entry::Occupied(mut occupied) if &item.timestamp > occupied.get() => { + trace!(?occupied, new_value=?item.timestamp, "New entry is more recent, replacing"); + occupied.insert(item.timestamp); + } + Entry::Occupied(occupied) => { + trace!(?occupied, new_value=?item.timestamp, "Previous entry is more recent, skipping"); + } + Entry::Vacant(v) => { + trace!(?item.timestamp, "No previous entry exists, inserting"); + v.insert(item.timestamp); + } + } + } + + pub fn write(&self, writer: W) -> Result<(), std::io::Error> { + let mut writer = BufWriter::new(writer); + let mut projects: Vec = self.inner.iter().map(Project::from).collect(); + + projects.sort(); + + projects + .into_iter() + .try_for_each(|project| writeln!(writer, "{project}")) + } +} + +impl Deref for Projects { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Projects { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } } -impl Generator for T -where - T: IntoIterator, -{ - type Error = Infallible; +impl Extend for Projects { + fn extend(&mut self, iter: T) + where + T: IntoIterator, + { + for path_buf in iter { + if let Some(project) = self.filters.filter_map(path_buf) { + self.insert(project) + } + } + } +} + +impl Extend for Projects { + fn extend(&mut self, iter: T) + where + T: IntoIterator, + { + for project in iter.into_iter() { + self.insert(project) + } + } +} - fn generate(self) -> Result, Self::Error> { - Ok(self.into_iter().collect()) +impl From for Projects { + fn from(value: crate::config::Projects) -> Self { + let mut projects = Projects::new(); + + if value.all { + projects.add_filter(PathMatcher::All); + } + + if let Some(pattern) = &value.pattern { + projects.add_filter(PathMatcher::Pattern(pattern.to_owned())); + } + + #[cfg(feature = "git")] + if value.git { + projects.add_filter(crate::git::Git); + } + + projects } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Project { pub timestamp: Duration, - pub worktree: PathBuf, + pub path_buf: PathBuf, +} + +impl Display for Project { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.path_buf.to_string_lossy()) + } +} + +impl From<(PathBuf, Duration)> for Project { + fn from((path_buf, timestamp): (PathBuf, Duration)) -> Self { + Self { + timestamp, + path_buf, + } + } +} + +impl From<(&PathBuf, &Duration)> for Project { + fn from((path_buf, timestamp): (&PathBuf, &Duration)) -> Self { + Self { + timestamp: *timestamp, + path_buf: path_buf.to_owned(), + } + } } impl TryFrom for Project { @@ -36,8 +155,9 @@ impl TryFrom for Project { .modified()? .duration_since(SystemTime::UNIX_EPOCH) .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err.to_string()))?; + Ok(Self { - worktree: value, + path_buf: value, timestamp, }) } diff --git a/src/search.rs b/src/search.rs index 27495b0..9f85f3a 100644 --- a/src/search.rs +++ b/src/search.rs @@ -3,66 +3,86 @@ use std::{ path::{Path, PathBuf}, }; -use ignore::{DirEntry, WalkBuilder}; -use tracing::{debug, error}; +use ignore::WalkBuilder; -use crate::{parser::Parser, project::Project}; - -pub struct SearchBuilder { - walk_builder: WalkBuilder, - projects: Vec, - parsers: Vec>, +pub struct Search { + inner: WalkBuilder, + paths: Vec, } -impl SearchBuilder { +impl Search { pub fn new>(path: P) -> Self { Self { - walk_builder: WalkBuilder::new(&path), - projects: Default::default(), - parsers: Default::default(), + inner: WalkBuilder::new(&path), + paths: Vec::from([path.as_ref().to_owned()]), } } - pub fn project>(&mut self, path: P) { - self.projects.push(path.as_ref().to_path_buf()) - } - - pub fn parser(&mut self, parser: impl Parser + 'static) { - self.parsers.push(Box::new(parser)) + pub fn add>(&mut self, path: P) { + self.paths.push(path.as_ref().to_owned()); + self.inner.add(path); } - pub fn build(mut self) -> impl Iterator { - self.walk_builder + pub fn build(mut self) -> impl Iterator { + self.inner .standard_filters(true) .build() - .inspect(|res| { - if let Err(err) = res { - error!(%err, "Ignoring errored path"); + .flat_map(move |res| match res.map(|d| d.into_path()) { + Ok(p) if self.paths.contains(&p) => { + tracing::debug!(?p, "Ignoring search directory"); + None } - }) - .flatten() - .map(DirEntry::into_path) - .chain(self.projects.into_iter()) - .map(move |p| self.parsers.parse(p)) - .inspect(|res| { - if let Err(err) = res { - debug!(%err, "Failed to match"); + Ok(p) => Some(p), + Err(err) => { + tracing::error!(%err, "Ignoring errored path"); + None } }) - .flatten() } } -impl Deref for SearchBuilder { +impl Deref for Search { type Target = WalkBuilder; fn deref(&self) -> &Self::Target { - &self.walk_builder + &self.inner } } -impl DerefMut for SearchBuilder { +impl DerefMut for Search { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.walk_builder + &mut self.inner + } +} + +impl IntoIterator for Search { + type Item = PathBuf; + + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.build().collect::>().into_iter() + } +} + +impl TryFrom for Search { + type Error = (); + + fn try_from(value: crate::config::Search) -> Result { + if value.paths.is_empty() { + return Err(()); + } + + let (init, paths) = value.paths.split_first().unwrap(); + let mut search = Search::new(init); + + for path in paths { + search.add(path); + } + + search.max_depth(value.max_depth); + search.hidden(!value.hidden); + + Ok(search) } } -- cgit v1.2.3-70-g09d2