summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorToby Vincent <tobyv13@gmail.com>2023-04-28 17:32:24 -0500
committerToby Vincent <tobyv13@gmail.com>2023-04-28 17:32:24 -0500
commit589cd987217755e1da7acbc304c373a75a9f7db5 (patch)
tree2730a697e4f7b2f779fcd96b0452719cc67f36e3 /src
parenta98b50667bc6a11e4b8be464969adc14601e9e78 (diff)
refactor: simplify logic and clean up dep.
Diffstat (limited to 'src')
-rw-r--r--src/config.rs47
-rw-r--r--src/error.rs20
-rw-r--r--src/lib.rs2
-rw-r--r--src/main.rs28
-rw-r--r--src/project.rs82
-rw-r--r--src/project/git.rs105
-rw-r--r--src/project/path.rs57
-rw-r--r--src/search.rs77
-rw-r--r--src/search/entry.rs40
9 files changed, 195 insertions, 263 deletions
diff --git a/src/config.rs b/src/config.rs
index 358a258..61a0778 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,20 +1,59 @@
+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<PathBuf>,
+
+ /// (UNIMPLEMENTED) Additional directories to add to the sorted output.
+ #[arg(long)]
+ pub add: Vec<PathBuf>,
+
#[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<usize>,
+
+ /// 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>.
+ ///
+ /// PATTERN should be a path relative to the searched directory.
+ #[arg(long, short)]
+ pub pattern: Option<String>,
+
+ /// 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<T> = std::result::Result<T, Error>;
-
-#[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<Project> = Search::new(config).into_iter().collect();
- res
-}
-
-#[tracing::instrument]
-pub async fn run(config: Config) -> Result<()> {
- let mut projects: Vec<ProjectItem> = 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<Box<dyn ProjectParser>>;
-
pub trait ProjectParser {
- fn parse(&self, path_buf: PathBuf) -> Option<ProjectItem>;
+ fn parse(&self, path_buf: PathBuf) -> Result<Project, Box<dyn std::error::Error>>;
}
-impl ProjectParser for ProjectParserGroup {
- fn parse(&self, path_buf: std::path::PathBuf) -> Option<ProjectItem> {
- self.iter().find_map(|p| p.parse(path_buf.to_owned()))
+#[derive(Default)]
+pub struct ProjectParserGroup {
+ pub parsers: Vec<Box<dyn ProjectParser>>,
+}
+
+impl ProjectParserGroup {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn parse(&self, path_buf: std::path::PathBuf) -> Option<Project> {
+ 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<dyn Project>;
+impl Deref for ProjectParserGroup {
+ type Target = Vec<Box<dyn ProjectParser>>;
-pub trait Project: Timestamp {
- fn to_path_buf(&self) -> &PathBuf;
+ fn deref(&self) -> &Self::Target {
+ &self.parsers
+ }
}
-impl<T> Project for T
-where
- T: Timestamp,
- T: AsRef<PathBuf>,
-{
- 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<T> Timestamp for T
-where
- T: AsRef<Duration>,
-{
- fn timestamp(&self) -> &Duration {
- self.as_ref()
+impl TryFrom<PathBuf> for Project {
+ type Error = std::io::Error;
+
+ fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
+ 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<super::ProjectItem> {
- 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<Project, Box<dyn std::error::Error>> {
+ 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<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, _> {
+ let (branch, _) = branch?;
-impl GitProject {
- fn new(path_buf: PathBuf) -> Result<Self> {
- 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<u64> {
- 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<PathBuf> for GitProject {
- fn as_ref(&self) -> &PathBuf {
- &self.path_buf
- }
-}
-
-impl TryFrom<PathBuf> for GitProject {
- type Error = Error;
-
- fn try_from(value: PathBuf) -> Result<Self> {
- Self::new(value)
- }
-}
-
-impl TryFrom<DirEntry> for GitProject {
- type Error = Error;
-
- fn try_from(value: DirEntry) -> Result<Self> {
- Self::new(value.into_path())
- }
-}
-
-impl From<GitProject> 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<ProjectItem> {
- 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<Duration, std::io::Error> {
- 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<PathBuf> for PathProject {
- fn as_ref(&self) -> &PathBuf {
- &self.0
- }
-}
-
-impl AsRef<Duration> for PathProject {
- fn as_ref(&self) -> &Duration {
- &self.1
+ 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))),
+ };
+
+ 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<PathBuf>;
-#[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<PathBuf>,
-
- #[command(flatten)]
- pub filter: Filters,
+ pub add: Vec<PathBuf>,
+ 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<usize>,
-
- /// 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 <PATTERN>
- #[arg(long, short)]
- pub pattern: Option<String>,
-
- /// 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<SearchEntry>,
-}
-
-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<SearchPath>,
}
impl Iterator for SearchIter {
- type Item = ProjectItem;
+ type Item = Project;
#[tracing::instrument]
fn next(&mut self) -> Option<Self::Item> {
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<Self::Item> {
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");