aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/config.rs33
-rw-r--r--src/git.rs29
-rw-r--r--src/lib.rs5
-rw-r--r--src/main.rs46
-rw-r--r--src/parser.rs45
-rw-r--r--src/path.rs16
-rw-r--r--src/project.rs146
-rw-r--r--src/search.rs92
8 files changed, 268 insertions, 144 deletions
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<PathBuf>,
+ #[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<PathBuf>,
#[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<PathBuf>,
+
/// 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<usize>,
-
- #[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>.
///
- /// 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<String>,
/// 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<Project, Box<dyn std::error::Error>> {
+ fn parse(&self, path_buf: PathBuf) -> Result<Project, Self::Error> {
Repository::open(&path_buf)?.parse(path_buf)
}
}
impl Parser for Repository {
- fn parse(&self, path_buf: PathBuf) -> Result<Project, Box<dyn std::error::Error>> {
- let mut branches = self.branches(Some(BranchType::Local))?;
- let timestamp = branches
- .try_fold(0, |latest, branch| -> std::result::Result<u64, _> {
+ type Error = Error;
+
+ fn parse(&self, path_buf: PathBuf) -> Result<Project, Self::Error> {
+ 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<Project> = 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<Item = Project> {
- 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<Project, Self::Error>;
}
-impl std::error::Error for NotMatched {}
+pub trait FilterMap {
+ fn filter_map(&self, path_buf: PathBuf) -> Option<Project>;
+}
-pub trait Parser {
- fn parse(&self, path_buf: PathBuf) -> Result<Project, Box<dyn std::error::Error>>;
+impl<T> FilterMap for T
+where
+ T: Parser,
+{
+ fn filter_map(&self, path_buf: PathBuf) -> Option<Project> {
+ match self.parse(path_buf) {
+ Ok(p) => Some(p),
+ Err(err) => {
+ tracing::info!(%err, "Parser failed to parse project");
+ None
+ }
+ }
+ }
}
-impl Parser for Vec<Box<dyn Parser>> {
- fn parse(&self, path_buf: PathBuf) -> Result<Project, Box<dyn std::error::Error>> {
+impl FilterMap for Vec<Box<dyn FilterMap>> {
+ fn filter_map(&self, path_buf: PathBuf) -> Option<Project> {
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<Project, Box<dyn std::error::Error>> {
- 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<Project, Self::Error> {
+ 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<Vec<Project>, Self::Error>;
+use crate::{parser::FilterMap, path::PathMatcher};
+
+#[derive(Default)]
+pub struct Projects {
+ inner: HashMap<PathBuf, Duration>,
+ filters: Vec<Box<dyn FilterMap>>,
+}
+
+impl Projects {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn add_filter<T: FilterMap + 'static>(&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<W: Write>(&self, writer: W) -> Result<(), std::io::Error> {
+ let mut writer = BufWriter::new(writer);
+ let mut projects: Vec<Project> = 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<PathBuf, Duration>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+}
+
+impl DerefMut for Projects {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.inner
+ }
}
-impl<T> Generator for T
-where
- T: IntoIterator<Item = Project>,
-{
- type Error = Infallible;
+impl Extend<PathBuf> for Projects {
+ fn extend<T>(&mut self, iter: T)
+ where
+ T: IntoIterator<Item = PathBuf>,
+ {
+ for path_buf in iter {
+ if let Some(project) = self.filters.filter_map(path_buf) {
+ self.insert(project)
+ }
+ }
+ }
+}
+
+impl Extend<Project> for Projects {
+ fn extend<T>(&mut self, iter: T)
+ where
+ T: IntoIterator<Item = Project>,
+ {
+ for project in iter.into_iter() {
+ self.insert(project)
+ }
+ }
+}
- fn generate(self) -> Result<Vec<Project>, Self::Error> {
- Ok(self.into_iter().collect())
+impl From<crate::config::Projects> 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<PathBuf> for Project {
@@ -36,8 +155,9 @@ impl TryFrom<PathBuf> 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<PathBuf>,
- parsers: Vec<Box<dyn Parser>>,
+pub struct Search {
+ inner: WalkBuilder,
+ paths: Vec<PathBuf>,
}
-impl SearchBuilder {
+impl Search {
pub fn new<P: AsRef<Path>>(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<P: AsRef<Path>>(&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<P: AsRef<Path>>(&mut self, path: P) {
+ self.paths.push(path.as_ref().to_owned());
+ self.inner.add(path);
}
- pub fn build(mut self) -> impl Iterator<Item = Project> {
- self.walk_builder
+ pub fn build(mut self) -> impl Iterator<Item = PathBuf> {
+ 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<Self::Item>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.build().collect::<Vec<_>>().into_iter()
+ }
+}
+
+impl TryFrom<crate::config::Search> for Search {
+ type Error = ();
+
+ fn try_from(value: crate::config::Search) -> Result<Self, Self::Error> {
+ 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)
}
}