diff options
-rw-r--r-- | Cargo.lock | 12 | ||||
-rw-r--r-- | Cargo.toml | 9 | ||||
-rw-r--r-- | src/lib.rs | 7 | ||||
-rw-r--r-- | src/main.rs | 4 | ||||
-rw-r--r-- | src/project.rs | 55 | ||||
-rw-r--r-- | src/project/error.rs | 1 | ||||
-rw-r--r-- | src/project/git.rs | 101 | ||||
-rw-r--r-- | src/project/path.rs | 78 | ||||
-rw-r--r-- | src/search.rs | 2 | ||||
-rw-r--r-- | src/search/entry.rs | 75 | ||||
-rw-r--r-- | src/search/entry/config.rs | 14 |
11 files changed, 188 insertions, 170 deletions
@@ -2405,7 +2405,6 @@ dependencies = [ "tmux_interface", "tracing", "tracing-subscriber", - "typed-builder", ] [[package]] @@ -3094,17 +3093,6 @@ dependencies = [ ] [[package]] -name = "typed-builder" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47a126a40dbff39e8320900cd61b8de053a2706e1f782cd27145792feb8fd41e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] name = "typenum" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -14,9 +14,9 @@ anyhow = "1.0.66" clap = { version = "4.0.18", features = ["derive", "env"] } dirs = "4.0.0" figment = { version = "0.10.8", features = ["toml", "env", "test"] } -git2 = { version = "0.15.0", default-features = false } +git2 = { version = "0.15.0", default-features = false, optional = true } ignore = "0.4.18" -onefetch = "2.14.2" +onefetch = { version = "2.14.2", optional = true } pretty_assertions = "1.3.0" serde = { version = "1.0.147", features = ["derive"] } sled = "0.34.7" @@ -30,3 +30,8 @@ tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } [dev-dependencies] tempfile = "3.3.0" + +[features] +default = ["git", "preview"] +git = ["dep:git2"] +preview = ["dep:onefetch"] @@ -1,11 +1,10 @@ pub use crate::cli::Cli; pub use crate::config::Config; pub use crate::error::{Error, Result}; -pub use crate::project::Project; -pub use crate::search::{entry, Search}; + +pub mod project; +pub mod search; mod cli; mod config; mod error; -mod project; -mod search; diff --git a/src/main.rs b/src/main.rs index a8bba5f..877cfaa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use clap::Parser; use figment::providers::{Env, Format, Toml}; -use projectr::{Cli, Config, Search}; +use projectr::{search::Search, Cli, Config}; #[tracing::instrument] fn main() -> Result<()> { @@ -29,7 +29,7 @@ pub fn run(config: &Config) -> Result<()> { .context("Failed to extract paths config")? .collect(); - projects.sort_unstable_by_key(|p| p.timestamp().unwrap_or_default()); + projects.sort_unstable_by_key(|p| *p.timestamp()); for project in projects { println!("{}", project.to_path_buf().to_string_lossy()) diff --git a/src/project.rs b/src/project.rs index e6faf66..5486dc1 100644 --- a/src/project.rs +++ b/src/project.rs @@ -1,19 +1,15 @@ -use std::{ - io::Write, - path::PathBuf, - process::{Command, Stdio}, - time::{Duration, SystemTime}, -}; +use std::{path::PathBuf, time::Duration}; pub use self::error::Error; -pub use self::git::GitProject; -pub use self::path::PathProject; mod error; -mod git; -mod path; +pub mod path; -pub type ProjectItem = Box<dyn Project<Error = Error>>; +#[cfg(feature = "git")] +pub mod git; + + +pub type ProjectItem = Box<dyn Project>; pub trait Project: Timestamp { fn to_path_buf(&self) -> &PathBuf; @@ -29,47 +25,26 @@ where } } -pub trait Timestamp { - type Error; +pub trait ProjectParser { + fn parse_project(&self, path_buf: PathBuf) -> Option<ProjectItem>; +} - fn timestamp(&self) -> Result<Duration, Self::Error>; +pub trait Timestamp { + fn timestamp(&self) -> &Duration; } impl<T> Timestamp for T where - T: AsRef<PathBuf>, + T: AsRef<Duration>, { - type Error = Error; - - fn timestamp(&self) -> Result<Duration, Self::Error> { + fn timestamp(&self) -> &Duration { self.as_ref() - .metadata()? - .modified()? - .duration_since(SystemTime::UNIX_EPOCH) - .map_err(Into::into) } } +#[cfg(feature = "preview")] pub trait Preview { type Error; fn preview(&self) -> Result<(), Self::Error>; } - -impl<T> Preview for T -where - T: AsRef<PathBuf>, -{ - type Error = std::io::Error; - - fn preview(&self) -> Result<(), Self::Error> { - let output = Command::new("ls") - .arg("-l") - .arg("-a") - .arg(self.to_path_buf()) - .stdout(Stdio::piped()) - .output()?; - - std::io::stdout().write_all(&output.stdout) - } -} diff --git a/src/project/error.rs b/src/project/error.rs index ddfcb2a..2e9f4a3 100644 --- a/src/project/error.rs +++ b/src/project/error.rs @@ -3,6 +3,7 @@ pub enum Error { #[error(transparent)] IO(#[from] std::io::Error), + #[cfg(feature = "git")] #[error(transparent)] Git(#[from] git2::Error), diff --git a/src/project/git.rs b/src/project/git.rs index 876af1c..efe4ade 100644 --- a/src/project/git.rs +++ b/src/project/git.rs @@ -2,71 +2,84 @@ use git2::{BranchType, Repository}; use ignore::DirEntry; use onefetch::ui::printer::Printer; use std::io; -use std::{cmp::Ordering, path::PathBuf, time::Duration}; +use std::{path::PathBuf, time::Duration}; +use tracing::{debug, warn}; -use crate::project::Error; -use crate::project::Preview; -use crate::project::Timestamp; -use crate::Project; +use super::{Error, Preview, ProjectParser, Timestamp}; + +#[derive(Debug, Clone)] +pub struct GitMatcher; + +impl ProjectParser for GitMatcher { + #[tracing::instrument] + fn parse_project(&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 + } + } + } +} #[derive(Debug, Clone)] pub struct GitProject { path_buf: PathBuf, - latest_commit: Option<Duration>, + latest_commit: Duration, } impl GitProject { fn new(path_buf: PathBuf) -> Result<Self, Error> { let repo = Repository::open(&path_buf)?; + let latest_commit = Self::get_timestamp(&repo); Ok(Self { path_buf, - latest_commit: Self::latest_commit(&repo).ok(), + latest_commit, }) } - fn latest_commit(repository: &Repository) -> Result<Duration, Error> { - let mut branches = repository.branches(Some(BranchType::Local))?; - branches - .try_fold(0, |latest, branch| { - let (branch, _) = branch?; - - 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(Duration::from_secs) - .map_err(Into::into) + 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() + } + } } -} -impl Timestamp for GitProject { - type Error = Error; + fn latest_commit(repository: &Repository) -> Result<u64, git2::Error> { + let mut branches = repository.branches(Some(BranchType::Local))?; + branches.try_fold(0, |latest, branch| { + let (branch, _) = branch?; - fn timestamp(&self) -> Result<Duration, Self::Error> { - self.latest_commit.ok_or(Error::Git(git2::Error::from_str( - "Failed to get latest commit", - ))) + 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)) + }) } } -impl Project for GitProject { - fn to_path_buf(&self) -> &PathBuf { - &self.path_buf +impl Timestamp for GitProject { + fn timestamp(&self) -> &Duration { + &self.latest_commit } } +#[cfg(feature = "preview")] impl Preview for GitProject { type Error = Error; fn preview(&self) -> Result<(), Self::Error> { - // onefetch --include-hidden --show-logo=auto let config = onefetch::cli::Config { input: self.path_buf.to_owned(), + include_hidden: true, ..Default::default() }; @@ -77,21 +90,9 @@ impl Preview for GitProject { } } -impl PartialEq for GitProject { - fn eq(&self, other: &Self) -> bool { - match (self.latest_commit, other.latest_commit) { - (Some(time), Some(other_time)) => time.eq(&other_time), - _ => false, - } - } -} - -impl PartialOrd for GitProject { - fn partial_cmp(&self, other: &Self) -> Option<Ordering> { - match (self.latest_commit, other.latest_commit) { - (Some(time), Some(other_time)) => time.partial_cmp(&other_time), - _ => None, - } +impl AsRef<PathBuf> for GitProject { + fn as_ref(&self) -> &PathBuf { + &self.path_buf } } diff --git a/src/project/path.rs b/src/project/path.rs index 14ce308..03bcac6 100644 --- a/src/project/path.rs +++ b/src/project/path.rs @@ -1,12 +1,39 @@ -use ignore::DirEntry; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use std::time::{Duration, SystemTime}; +use tracing::debug; + +use super::{ProjectItem, ProjectParser}; + +#[derive(Debug, Clone)] +pub struct PathMatcher(pub String); + +impl ProjectParser for PathMatcher { + #[tracing::instrument] + fn parse_project(&self, path_buf: PathBuf) -> Option<ProjectItem> { + if path_buf.join(&self.0).exists() { + Some(Box::new(PathProject::new(path_buf))) + } else { + debug!("Failed to match pattern in directory"); + None + } + } +} #[derive(Debug, PartialEq, Eq, Clone, Default)] -pub struct PathProject(PathBuf); +pub struct PathProject(PathBuf, Duration); impl PathProject { - fn new(path_buf: PathBuf) -> Self { - Self(path_buf) + 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())) } } @@ -16,32 +43,27 @@ impl AsRef<PathBuf> for PathProject { } } -impl TryFrom<(&String, DirEntry)> for PathProject { - type Error = String; - - fn try_from((pattern, dir_entry): (&String, DirEntry)) -> Result<Self, Self::Error> { - dir_entry - .path() - .join(pattern) - .exists() - .then(|| Self::from(dir_entry.to_owned())) - .ok_or_else(|| { - format!( - "Pattern `{:?}` not found in path: `{:?}`", - pattern, dir_entry - ) - }) +impl AsRef<Duration> for PathProject { + fn as_ref(&self) -> &Duration { + &self.1 } } -impl From<DirEntry> for PathProject { - fn from(value: DirEntry) -> Self { - Self::new(value.into_path()) - } -} +#[cfg(feature = "preview")] +impl super::Preview for PathProject { + type Error = std::io::Error; + + fn preview(&self) -> Result<(), Self::Error> { + use std::io::Write; + use std::process::{Command, Stdio}; + + let output = Command::new("ls") + .arg("-l") + .arg("-a") + .arg(&self.0) + .stdout(Stdio::piped()) + .output()?; -impl From<PathProject> for PathBuf { - fn from(value: PathProject) -> Self { - value.0 + std::io::stdout().write_all(&output.stdout) } } diff --git a/src/search.rs b/src/search.rs index 2aff5ae..b2db0e3 100644 --- a/src/search.rs +++ b/src/search.rs @@ -109,7 +109,7 @@ mod tests { let mut results = paths.into_iter().collect::<Vec<ProjectItem>>(); - results.sort_unstable_by_key(|p| p.timestamp().unwrap_or_default()); + results.sort_unstable_by_key(|p| *p.timestamp()); let results = results .into_iter() diff --git a/src/search/entry.rs b/src/search/entry.rs index 778c75c..1e49a67 100644 --- a/src/search/entry.rs +++ b/src/search/entry.rs @@ -1,49 +1,90 @@ -use ignore::{DirEntry, Walk}; -use tracing::error; +use ignore::{Walk, WalkBuilder}; +use tracing::{error, warn}; -use crate::project::{GitProject, PathProject, ProjectItem}; +use crate::{ + project::{path::PathMatcher, ProjectParser}, + search::ProjectItem, +}; pub use config::Config; mod config; pub struct Entry { - config: Config, + path_parser: Option<PathMatcher>, + + #[cfg(feature = "git")] + git_parser: Option<crate::project::git::GitMatcher>, + iter: Walk, } impl std::fmt::Debug for Entry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SearchPath") - .field("config", &self.config) - .finish() + let mut debug = f.debug_struct("Entry"); + debug.field("path_matcher", &self.path_parser); + + #[cfg(feature = "git")] + debug.field("git_matcher", &self.git_parser); + + debug.finish() } } impl Entry { - pub fn parse_dir_entry(&self, dir_entry: DirEntry) -> Option<ProjectItem> { - if self.config.git { - if let Ok(git) = GitProject::try_from(dir_entry.to_owned()) { - return Some(Box::new(git)); - }; + fn new(config: &Config) -> Self { + let iter = WalkBuilder::new(&config.path_buf) + .standard_filters(true) + .max_depth(config.max_depth) + .hidden(!config.hidden) + .build(); + + Self { + iter, + path_parser: config.pattern.as_ref().map(|s| PathMatcher(s.to_owned())), + + #[cfg(feature = "git")] + git_parser: config.git.then_some(crate::project::git::GitMatcher), + } + } +} + +impl ProjectParser for Entry { + #[tracing::instrument] + fn parse_project(&self, path_buf: std::path::PathBuf) -> Option<ProjectItem> { + #[cfg(feature = "git")] + if let Some(p) = self + .git_parser + .as_ref() + .and_then(|m| m.parse_project(path_buf.to_owned())) + { + return Some(p); }; - if let Some(pattern) = &self.config.pattern { - if let Ok(proj) = PathProject::try_from((pattern, dir_entry)) { - return Some(Box::new(proj)); - }; + if let Some(p) = self + .path_parser + .as_ref() + .and_then(|m| m.parse_project(path_buf)) + { + return Some(p); }; None } } +impl From<Config> for Entry { + fn from(config: Config) -> Self { + Self::new(&config) + } +} + impl Iterator for Entry { type Item = ProjectItem; fn next(&mut self) -> Option<Self::Item> { match self.iter.next()? { - Ok(dir_entry) => self.parse_dir_entry(dir_entry), + Ok(dir_entry) => self.parse_project(dir_entry.into_path()), Err(err) => { error!(%err, "Ignoring errored path"); None diff --git a/src/search/entry/config.rs b/src/search/entry/config.rs index d325b58..24c7971 100644 --- a/src/search/entry/config.rs +++ b/src/search/entry/config.rs @@ -1,9 +1,6 @@ -use ignore::WalkBuilder; use serde::{Deserialize, Deserializer, Serialize}; use std::{convert::Infallible, path::PathBuf, str::FromStr}; -use super::Entry; - #[derive(Debug, PartialEq, Eq, Clone, Default, Serialize)] #[serde(default)] pub struct Config { @@ -14,17 +11,6 @@ pub struct Config { pub pattern: Option<String>, } -impl From<Config> for Entry { - fn from(config: Config) -> Self { - let iter = WalkBuilder::new(&config.path_buf) - .standard_filters(true) - .max_depth(config.max_depth) - .hidden(!config.hidden) - .build(); - Self { iter, config } - } -} - impl From<PathBuf> for Config { fn from(path_buf: PathBuf) -> Self { Self { |