From 589cd987217755e1da7acbc304c373a75a9f7db5 Mon Sep 17 00:00:00 2001 From: Toby Vincent Date: Fri, 28 Apr 2023 17:32:24 -0500 Subject: refactor: simplify logic and clean up dep. --- src/config.rs | 47 +++++++++++++++++++++-- src/error.rs | 20 ---------- src/lib.rs | 2 - src/main.rs | 28 ++++---------- src/project.rs | 82 +++++++++++++++++++++++++++------------- src/project/git.rs | 105 +++++++++++----------------------------------------- src/project/path.rs | 57 ++++++---------------------- src/search.rs | 77 +++++++++++++------------------------- src/search/entry.rs | 40 ++++++++++++++------ 9 files changed, 195 insertions(+), 263 deletions(-) delete mode 100644 src/error.rs (limited to 'src') diff --git a/src/config.rs b/src/config.rs index 358a258..61a0778 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,19 +1,58 @@ +use std::path::PathBuf; + use clap::{Args, Parser}; use tracing::{metadata::LevelFilter, Level}; -use crate::search::Search; - -/// Tool for listing project directories. #[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, + + /// (UNIMPLEMENTED) Additional directories to add to the sorted output. + #[arg(long)] + pub add: Vec, + #[command(flatten)] - pub search: Search, + pub filters: Filters, #[command(flatten)] pub verbosity: Verbosity, } +#[derive(Debug, Default, Clone, Args)] +pub struct Filters { + /// Match all child directories. + #[arg(short, long)] + pub all: bool, + + /// Max depth to recurse. + /// + /// MAX_DEPTH of 0 will only return the supplied PATHS. + #[arg(short = 'd', long, default_value = "1")] + pub max_depth: Option, + + /// Recurse into hidden directories. + /// + /// Traverse into hidden directories while searching. A directory is considered hidden + /// if its name starts with a `.` sign (dot). If `--max-depth` is 0, this has no effect. + #[arg(long)] + pub hidden: bool, + + /// Match directories containing . + /// + /// PATTERN should be a path relative to the searched directory. + #[arg(long, short)] + pub pattern: Option, + + /// Match git repositories. + #[cfg(feature = "git")] + #[arg(long, short)] + pub git: bool, +} + #[derive(Debug, Default, Clone, Args)] pub struct Verbosity { /// Print additional information per occurrence. diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index c7b6448..0000000 --- a/src/error.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub type Result = std::result::Result; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error(transparent)] - Ignore(#[from] ignore::Error), - - #[error(transparent)] - IO(#[from] std::io::Error), - - #[cfg(feature = "git")] - #[error(transparent)] - Git(#[from] git2::Error), - - #[error(transparent)] - SystemTime(#[from] std::time::SystemTimeError), - - #[error(transparent)] - Other(#[from] anyhow::Error), -} diff --git a/src/lib.rs b/src/lib.rs index b54d897..8948cf0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,6 @@ pub use crate::config::Config; -pub use crate::error::{Error, Result}; pub mod project; pub mod search; mod config; -mod error; diff --git a/src/main.rs b/src/main.rs index 7f3b90d..7fbed15 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,34 +1,22 @@ -use anyhow::{Context, Result}; +use anyhow::Result; use clap::Parser; -use projectr::{project::ProjectItem, Config}; -use tokio::signal; +use projectr::{project::Project, search::Search, Config}; -#[tokio::main] -async fn main() -> Result<()> { - let cli = Config::parse(); +fn main() -> Result<()> { + let config = Config::parse(); tracing_subscriber::fmt::fmt() .pretty() .with_writer(std::io::stderr) - .with_max_level(&cli.verbosity) + .with_max_level(&config.verbosity) .init(); - let res = tokio::select! { - res = signal::ctrl_c() => res.map_err(Into::into), - res = run(cli) => res.context("Failed to run projectr"), - }; + let mut projects: Vec = Search::new(config).into_iter().collect(); - res -} - -#[tracing::instrument] -pub async fn run(config: Config) -> Result<()> { - let mut projects: Vec = config.search.into_iter().collect(); - - projects.sort_unstable_by_key(|p| *p.timestamp()); + projects.sort_unstable_by_key(|p| p.timestamp); for project in projects { - println!("{}", project.to_path_buf().to_string_lossy()) + println!("{}", project.worktree.to_string_lossy()) } Ok(()) diff --git a/src/project.rs b/src/project.rs index 26f0a8b..2b3e028 100644 --- a/src/project.rs +++ b/src/project.rs @@ -1,47 +1,79 @@ -use std::{path::PathBuf, time::Duration}; +use std::{ + ops::{Deref, DerefMut}, + path::PathBuf, + time::{Duration, SystemTime}, +}; + +use tracing::warn; pub mod path; #[cfg(feature = "git")] pub mod git; -pub type ProjectParserGroup = Vec>; - pub trait ProjectParser { - fn parse(&self, path_buf: PathBuf) -> Option; + fn parse(&self, path_buf: PathBuf) -> Result>; } -impl ProjectParser for ProjectParserGroup { - fn parse(&self, path_buf: std::path::PathBuf) -> Option { - self.iter().find_map(|p| p.parse(path_buf.to_owned())) +#[derive(Default)] +pub struct ProjectParserGroup { + pub parsers: Vec>, +} + +impl ProjectParserGroup { + pub fn new() -> Self { + Self::default() + } + + pub fn parse(&self, path_buf: std::path::PathBuf) -> Option { + if self.parsers.is_empty() { + return path_buf.try_into().ok(); + } + + self.iter() + .map(|p| p.parse(path_buf.to_owned())) + .inspect(|res| { + if let Err(err) = res { + warn!(%err, "Parser failed to match"); + } + }) + .flatten() + .reduce(|max, p| p.max(max)) } } -pub type ProjectItem = Box; +impl Deref for ProjectParserGroup { + type Target = Vec>; -pub trait Project: Timestamp { - fn to_path_buf(&self) -> &PathBuf; + fn deref(&self) -> &Self::Target { + &self.parsers + } } -impl Project for T -where - T: Timestamp, - T: AsRef, -{ - fn to_path_buf(&self) -> &PathBuf { - self.as_ref() +impl DerefMut for ProjectParserGroup { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.parsers } } -pub trait Timestamp { - fn timestamp(&self) -> &Duration; +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Project { + pub timestamp: Duration, + pub worktree: PathBuf, } -impl Timestamp for T -where - T: AsRef, -{ - fn timestamp(&self) -> &Duration { - self.as_ref() +impl TryFrom for Project { + type Error = std::io::Error; + + fn try_from(value: PathBuf) -> Result { + let timestamp = value + .metadata()? + .modified()? + .duration_since(SystemTime::UNIX_EPOCH) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err.to_string()))?; + Ok(Self { + worktree: value, + timestamp, + }) } } diff --git a/src/project/git.rs b/src/project/git.rs index 4584a7a..e2067ec 100644 --- a/src/project/git.rs +++ b/src/project/git.rs @@ -1,102 +1,39 @@ use git2::{BranchType, Repository}; -use ignore::DirEntry; use std::{path::PathBuf, time::Duration}; -use tracing::{debug, warn}; -use crate::{Error, Result}; - -use super::{ProjectParser, Timestamp}; +use super::{Project, ProjectParser}; #[derive(Debug, Clone)] pub struct GitMatcher; impl ProjectParser for GitMatcher { #[tracing::instrument] - fn parse(&self, path_buf: PathBuf) -> Option { - match GitProject::new(path_buf) { - Ok(g) => Some(Box::new(g)), - Err(err) => { - debug!(%err, "Failed to create git project"); - None - } - } + fn parse(&self, path_buf: PathBuf) -> Result> { + Repository::open(&path_buf)?.parse(path_buf) } } -#[derive(Debug, Clone)] -pub struct GitProject { - path_buf: PathBuf, - latest_commit: Duration, -} +impl ProjectParser 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 { + let (branch, _) = branch?; -impl GitProject { - fn new(path_buf: PathBuf) -> Result { - let repo = Repository::open(&path_buf)?; - let latest_commit = Self::get_timestamp(&repo); - Ok(Self { - path_buf, - latest_commit, - }) - } - - fn get_timestamp(repo: &Repository) -> Duration { - match Self::latest_commit(repo) { - Ok(s) => Duration::from_secs(s), - Err(err) => { - warn!(%err, "Failed to get latest commit from repository"); - Duration::default() - } - } - } + let name = branch + .name()? + .ok_or_else(|| git2::Error::from_str("Failed to find branch"))?; - fn latest_commit(repository: &Repository) -> Result { - let mut branches = repository.branches(Some(BranchType::Local))?; - branches.try_fold(0, |latest, branch| { - let (branch, _) = branch?; + self.revparse_single(name)? + .peel_to_commit() + .map(|c| (c.time().seconds() as u64).max(latest)) + }) + .map(Duration::from_secs) + .unwrap_or_default(); - let name = branch - .name()? - .ok_or_else(|| git2::Error::from_str("Failed to find branch"))?; - - repository - .revparse_single(name)? - .peel_to_commit() - .map(|c| (c.time().seconds() as u64).max(latest)) - .map_err(Into::into) + Ok(Project { + worktree: path_buf, + timestamp, }) } } - -impl Timestamp for GitProject { - fn timestamp(&self) -> &Duration { - &self.latest_commit - } -} - -impl AsRef for GitProject { - fn as_ref(&self) -> &PathBuf { - &self.path_buf - } -} - -impl TryFrom for GitProject { - type Error = Error; - - fn try_from(value: PathBuf) -> Result { - Self::new(value) - } -} - -impl TryFrom for GitProject { - type Error = Error; - - fn try_from(value: DirEntry) -> Result { - Self::new(value.into_path()) - } -} - -impl From for PathBuf { - fn from(value: GitProject) -> Self { - value.path_buf - } -} diff --git a/src/project/path.rs b/src/project/path.rs index 5954ff9..0e38990 100644 --- a/src/project/path.rs +++ b/src/project/path.rs @@ -1,57 +1,22 @@ -use std::path::{Path, PathBuf}; -use std::time::{Duration, SystemTime}; -use tracing::debug; +use std::{io::ErrorKind, path::PathBuf}; -use super::{ProjectItem, ProjectParser}; +use super::{Project, ProjectParser}; #[derive(Debug, Clone)] pub enum PathMatcher { - All(PathBuf), + All, Pattern(String), } impl ProjectParser for PathMatcher { #[tracing::instrument] - fn parse(&self, path_buf: PathBuf) -> Option { - match self { - PathMatcher::All(p) if &path_buf != p => Some(Box::new(PathProject::new(path_buf))), - PathMatcher::Pattern(p) if path_buf.join(p).exists() => { - Some(Box::new(PathProject::new(path_buf))) - } - _ => { - debug!("Failed to match pattern in directory"); - None - } - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Default)] -pub struct PathProject(PathBuf, Duration); - -impl PathProject { - pub fn new(path_buf: PathBuf) -> Self { - let modified = Self::get_modified(&path_buf).unwrap_or_default(); - Self(path_buf, modified) - } - - fn get_modified(path_buf: &Path) -> Result { - path_buf - .metadata()? - .modified()? - .duration_since(SystemTime::UNIX_EPOCH) - .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err.to_string())) - } -} - -impl AsRef for PathProject { - fn as_ref(&self) -> &PathBuf { - &self.0 - } -} - -impl AsRef for PathProject { - fn as_ref(&self) -> &Duration { - &self.1 + 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))), + }; + + Ok(project) } } diff --git a/src/search.rs b/src/search.rs index 888179e..81049e9 100644 --- a/src/search.rs +++ b/src/search.rs @@ -1,92 +1,67 @@ use std::path::PathBuf; -use clap::Args; +use crate::{config::Filters, project::Project, Config}; -use crate::project::ProjectItem; - -use self::entry::SearchEntry; +use self::entry::SearchPath; pub mod entry; type EntryIter = std::vec::IntoIter; -#[derive(Debug, Default, Clone, Args)] +#[derive(Debug, Default, Clone)] pub struct Search { - /// Directory to search. - /// - /// Directories are searched recursively based on `--max-depth`. pub paths: Vec, - - #[command(flatten)] - pub filter: Filters, + pub add: Vec, + pub filters: Filters, } -#[derive(Debug, Default, Clone, Args)] -pub struct Filters { - /// Match all child directories - #[arg(long, short, conflicts_with_all = ["pattern", "git"])] - pub all: bool, - - /// Max depth to recurse. - /// - /// Setting to 0 will only use the supplied directory. - #[arg(short = 'd', long, default_value = "1")] - pub max_depth: Option, - - /// Recurse into hidden directories. - /// - /// Traverse into hidden directories while searching. A directory is considered hidden - /// if its name starts with a `.` sign (dot). If `--max-depth` is 0, this has no effect. - #[arg(long)] - pub hidden: bool, - - /// Match directories containing item named - #[arg(long, short)] - pub pattern: Option, - - /// Match git repositories - #[cfg(feature = "git")] - #[arg(long, short, default_value_t = true)] - pub git: bool, +impl Search { + pub fn new( + Config { + paths, + add, + filters, + verbosity: _, + }: Config, + ) -> Self { + Self { + paths, + add, + filters, + } + } } impl IntoIterator for Search { - type Item = ProjectItem; + type Item = Project; type IntoIter = SearchIter; fn into_iter(self) -> Self::IntoIter { SearchIter { iter: self.paths.into_iter(), - config: self.filter, + config: self.filters, curr: None, } } } +#[derive(Debug)] pub struct SearchIter { iter: EntryIter, config: Filters, - curr: Option, -} - -impl std::fmt::Debug for SearchIter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Projects") - .field("paths_iter", &self.iter) - .finish_non_exhaustive() - } + curr: Option, } impl Iterator for SearchIter { - type Item = ProjectItem; + type Item = Project; #[tracing::instrument] fn next(&mut self) -> Option { match self.curr.as_mut().and_then(|c| c.next()) { Some(proj) => Some(proj), None => { - self.curr = Some(SearchEntry::new(self.iter.next()?, &self.config)); + self.curr = Some(SearchPath::new(self.iter.next()?, &self.config)); self.next() } } diff --git a/src/search/entry.rs b/src/search/entry.rs index 16dcd8b..efd287b 100644 --- a/src/search/entry.rs +++ b/src/search/entry.rs @@ -1,23 +1,33 @@ use std::path::PathBuf; use ignore::{Walk, WalkBuilder}; -use tracing::error; +use tracing::{debug, error}; -use crate::project::{path::PathMatcher, ProjectItem, ProjectParser, ProjectParserGroup}; +use crate::{ + config::Filters, + project::{path::PathMatcher, Project, ProjectParserGroup}, +}; -use super::Filters; - -pub struct SearchEntry { +pub struct SearchPath { + path_buf: PathBuf, parsers: ProjectParserGroup, iter: Walk, } -impl SearchEntry { +impl std::fmt::Debug for SearchPath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SearchPath") + .field("path_buf", &self.path_buf) + .finish() + } +} + +impl SearchPath { pub fn new(path_buf: PathBuf, config: &Filters) -> Self { let mut parsers = ProjectParserGroup::new(); if config.all { - parsers.push(Box::new(PathMatcher::All(path_buf.to_owned()))) + parsers.push(Box::new(PathMatcher::All)) } if let Some(s) = config.pattern.as_ref() { @@ -29,21 +39,29 @@ impl SearchEntry { parsers.push(Box::new(crate::project::git::GitMatcher)); }; - let iter = WalkBuilder::new(path_buf) + let iter = WalkBuilder::new(&path_buf) .standard_filters(true) .max_depth(config.max_depth) .hidden(!config.hidden) .build(); - Self { parsers, iter } + Self { + path_buf, + parsers, + iter, + } } } -impl Iterator for SearchEntry { - type Item = ProjectItem; +impl Iterator for SearchPath { + type Item = Project; fn next(&mut self) -> Option { match self.iter.next()? { + Ok(dir_entry) if dir_entry.path() == self.path_buf => { + debug!("Ignoring parent directory"); + None + } Ok(dir_entry) => self.parsers.parse(dir_entry.into_path()), Err(err) => { error!(%err, "Ignoring errored path"); -- cgit v1.2.3-70-g09d2