diff options
author | Nick Gerace <nickagerace@gmail.com> | 2022-03-25 17:42:40 -0400 |
---|---|---|
committer | Nick Gerace <nickagerace@gmail.com> | 2022-03-25 17:44:40 -0400 |
commit | bcd37c3e8f3b13510a7947327e7723e56c1a1284 (patch) | |
tree | b530858ad7d883d05155e7d1211d4a5bd336c187 | |
parent | 7ea0a538cbc2924de7c0baf12222767a3e1ace48 (diff) | |
download | gfold-bcd37c3e8f3b13510a7947327e7723e56c1a1284.zip |
Revert "Revert accidental WIP commit"
This reverts commit 7ea0a538cbc2924de7c0baf12222767a3e1ace48.
-rw-r--r-- | Cargo.lock | 77 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/color.rs | 74 | ||||
-rw-r--r-- | src/config.rs | 2 | ||||
-rw-r--r-- | src/logging.rs | 17 | ||||
-rw-r--r-- | src/main.rs | 25 | ||||
-rw-r--r-- | src/mod.rs | 213 | ||||
-rw-r--r-- | src/result.rs | 6 | ||||
-rw-r--r-- | src/run.rs | 2 | ||||
-rw-r--r-- | src/target.rs | 68 |
10 files changed, 105 insertions, 380 deletions
@@ -3,6 +3,30 @@ version = 3 [[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "anyhow" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" +dependencies = [ + "backtrace", +] + +[[package]] name = "argh" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -49,6 +73,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] +name = "backtrace" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e121dee8023ce33ab248d9ce1493df03c3b38a659b240096fcbd7048ff9c31f" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -175,6 +214,7 @@ dependencies = [ name = "gfold" version = "4.0.0-rc.1" dependencies = [ + "anyhow", "argh", "dirs", "env_logger", @@ -189,6 +229,12 @@ dependencies = [ ] [[package]] +name = "gimli" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" + +[[package]] name = "git2" version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -303,6 +349,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] name = "memoffset" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -312,6 +364,16 @@ dependencies = [ ] [[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] name = "num_cpus" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -322,6 +384,15 @@ dependencies = [ ] [[package]] +name = "object" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ac1d3f9a1d3616fd9a60c8d74296f22406a238b6a72f5cc1e6f314df4ffbf9" +dependencies = [ + "memchr", +] + +[[package]] name = "percent-encoding" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -396,6 +467,12 @@ dependencies = [ ] [[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] name = "ryu" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -14,6 +14,7 @@ version = "4.0.0-rc.1" rust-version = "1.56.1" [dependencies] +anyhow = { version = "1", features = ["backtrace"] } argh = "0" dirs = "4" git2 = { version = "0", default_features = false } diff --git a/src/color.rs b/src/color.rs deleted file mode 100644 index 47ae7a0..0000000 --- a/src/color.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! This module provides a harness for non-trivial displays of information to `stdout`. - -use crate::config::ColorMode; -use crate::status::Status; -use std::io::{self, Write}; -use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; - -/// This harness provides methods to write to `stdout`. It maps the internal [`ColorMode`] type to -/// our dependency's [`ColorChoice`] type due to discrepancies in behavior and naming. -pub struct ColorHarness { - color_choice: ColorChoice, -} - -impl ColorHarness { - pub fn new(color_mode: &ColorMode) -> Self { - Self { - color_choice: match &color_mode { - ColorMode::Always => ColorChoice::Always, - ColorMode::Compatibility => ColorChoice::Auto, - ColorMode::Never => ColorChoice::Never, - }, - } - } - - /// Writes the [`Status`] of the Git repository to `stdout`. - pub fn write_status(&self, status: &Status, status_width: usize) -> io::Result<()> { - let mut stdout = StandardStream::stdout(self.color_choice); - stdout.set_color(ColorSpec::new().set_fg(Some(match status { - Status::Bare => Color::Red, - Status::Clean => Color::Green, - _ => Color::Yellow, - })))?; - write!( - &mut stdout, - "{:<status_width$}", - status.as_str(), - status_width = status_width, - )?; - stdout.reset() - } - - /// Writes the input [`&str`] to `stdout` in bold. - pub fn write_bold(&self, input: &str, newline: bool) -> io::Result<()> { - self.write_color(input, newline, ColorSpec::new().set_bold(true)) - } - - /// Writes the input [`&str`] to `stdout` in gray (or cyan if in compatibility mode). - pub fn write_gray(&self, input: &str, newline: bool) -> io::Result<()> { - // FIXME: check why Color::Rg(128, 128, 128) breaks in tmux on macOS Terminal.app. - self.write_color( - input, - newline, - ColorSpec::new().set_fg(Some(match &self.color_choice { - ColorChoice::Auto => Color::Cyan, - _ => Color::Ansi256(242), - })), - ) - } - - fn write_color( - &self, - input: &str, - newline: bool, - color_spec: &mut ColorSpec, - ) -> io::Result<()> { - let mut stdout = StandardStream::stdout(self.color_choice); - stdout.set_color(color_spec)?; - match newline { - true => writeln!(&mut stdout, "{}", input)?, - false => write!(&mut stdout, "{}", input)?, - } - stdout.reset() - } -} diff --git a/src/config.rs b/src/config.rs index f898d55..953d1a2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ //! This module contains the config specification and functionality for creating a config. use crate::error::Error; -use crate::result::Result; +use anyhow::Result; use serde::{Deserialize, Serialize}; use std::env; use std::path::PathBuf; diff --git a/src/logging.rs b/src/logging.rs deleted file mode 100644 index f0f1ac6..0000000 --- a/src/logging.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! This module contains the logger initialization logic. - -use env_logger::Builder; -use log::LevelFilter; -use std::env; - -/// Initialize the logger based on the debug flag and `RUST_LOG` environment variable. The flag -/// takes precedence over the environment variable. -pub fn init(debug: bool) { - match debug { - true => Builder::new().filter_level(LevelFilter::Debug).init(), - false => match env::var("RUST_LOG").is_err() { - true => Builder::new().filter_level(LevelFilter::Off).init(), - false => env_logger::init(), - }, - } -} diff --git a/src/main.rs b/src/main.rs index 4797f6c..87b100a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ //! track of multiple Git repositories. The source code uses private modules rather than leveraging //! a library via `lib.rs`. -use crate::result::Result; +use anyhow::Result; mod cli; mod config; @@ -15,7 +15,9 @@ mod status; /// Calls [`cli::parse_and_run()`] to generate a [`config::Config`] and eventually call [`run::run()`]; fn main() -> Result<()> { - cli::parse_and_run() + cli::parse_and_run()?; + panic!( + Ok(()) } #[cfg(test)] @@ -26,6 +28,7 @@ mod tests { use git2::Repository; use std::path::Path; use std::{env, fs, io}; + use git2::ErrorCode; #[test] fn integration() { @@ -85,11 +88,27 @@ mod tests { for name in ["one", "two", "three"] { let current = nested.join(name); create_dir_or_die(¤t); - Repository::init(¤t).expect("could not initialize repository"); + let repository = Repository::init(¤t).expect("could not initialize repository"); if name == "one" { create_file_or_die(¤t.join("newfile")); } + + if name == "two" { + if let Err(e) = repository.remote("origin", "https://google.com") { + if e.code() != ErrorCode::Exists { + panic!("{}", e); + } + } + } + + if name == "three" { + if let Err(e) = repository.remote("fork", "https://google.com") { + if e.code() != ErrorCode::Exists { + panic!("{}", e); + } + } + } } let mut config = Config::new().expect("could not create new config"); diff --git a/src/mod.rs b/src/mod.rs deleted file mode 100644 index aadab52..0000000 --- a/src/mod.rs +++ /dev/null @@ -1,213 +0,0 @@ -//! This module contains the functionality for generating reports. - -use crate::config::DisplayMode; -use crate::error::Error; -use crate::result::Result; -use crate::status::Status; -use git2::{ErrorCode, Reference, Repository, StatusOptions}; -use log::{debug, trace}; -use rayon::prelude::*; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; -use std::path::Path; - -mod target; - -const HEAD: &str = "HEAD"; - -/// This type represents a [`BTreeMap`] using an optional [`String`] for keys, which represents the -/// parent directory for a group of reports ([`Vec<Report>`]). The values corresponding to those keys -/// are the actual groups of reports. -// NOTE: We use a BTreeMap over a HashMap for sorted keys. -pub type Reports = BTreeMap<Option<String>, Vec<Report>>; - -/// A collection of results for a Git repository at a given path. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Report { - /// The directory name of the Git repository. - pub name: String, - /// The name of the current, open branch. - pub branch: String, - /// The [`Status`] of the working tree. - pub status: Status, - - /// The parent directory of the `path` field. The value will be `None` if a parent is not found. - pub parent: Option<String>, - /// The remote origin URL. The value will be `None` if the URL cannot be found. - pub url: Option<String>, - - /// The "user.email" of a Git config that's only collected when using [`DisplayMode::Standard`]. - pub email: Option<String>, -} - -impl Report { - fn new( - path: &Path, - branch: &str, - status: &Status, - url: Option<String>, - email: Option<String>, - ) -> Result<Self> { - Ok(Self { - name: match path.file_name() { - Some(s) => match s.to_str() { - Some(s) => s.to_string(), - None => return Err(Error::FileNameToStrConversionFailure(path.to_path_buf())), - }, - None => return Err(Error::FileNameNotFound(path.to_path_buf())), - }, - branch: (*branch).into(), - status: *status, - parent: match path.parent() { - Some(s) => match s.to_str() { - Some(s) => Some(s.to_string()), - None => return Err(Error::PathToStrConversionFailure(s.to_path_buf())), - }, - None => None, - }, - url, - email, - }) - } -} - -/// Generate [`Reports`] for a given path and its children. The [`DisplayMode`] is required because -/// any two display modes can require differing ammounts of data to be collected. -pub fn generate_reports(path: &Path, display_mode: &DisplayMode) -> Result<Reports> { - let include_email = match display_mode { - DisplayMode::Standard | DisplayMode::Json => true, - DisplayMode::Classic => false, - }; - - let unprocessed = target::recursive_target_gen(path)? - .par_iter() - .map(|path| generate_report(path, include_email)) - .collect::<Vec<Result<Report>>>(); - - let mut processed = Reports::new(); - for wrapped_report in unprocessed { - match wrapped_report { - Ok(report) => { - if let Some(mut v) = processed.insert(report.parent.clone(), vec![report.clone()]) { - v.push(report.clone()); - processed.insert(report.parent, v); - } - } - Err(e) => return Err(e), - } - } - Ok(processed) -} - -/// Generates a report with a given path. -fn generate_report(repo_path: &Path, include_email: bool) -> Result<Report> { - debug!("attemping to generate report for path: {:?}", repo_path); - let repo = Repository::open(repo_path)?; - let head = match repo.head() { - Ok(head) => Some(head), - Err(ref e) if e.code() == ErrorCode::UnbornBranch || e.code() == ErrorCode::NotFound => { - None - } - Err(e) => return Err(Error::Git2Rs(e)), - }; - let branch = match &head { - Some(head) => head - .shorthand() - .ok_or(Error::GitReferenceShorthandInvalid)?, - None => HEAD, - }; - - // We'll include all untracked files and directories in the status options. - let mut opts = StatusOptions::new(); - opts.include_untracked(true).recurse_untracked_dirs(true); - - // If "head" is "None" and statuses are empty, then the repository must be clean because there - // are no commits to push. - let status = match repo.statuses(Some(&mut opts)) { - Ok(v) if v.is_empty() => match &head { - Some(head) => match is_unpushed(&repo, head)? { - true => Status::Unpushed, - false => Status::Clean, - }, - None => Status::Clean, - }, - Ok(_) => Status::Unclean, - Err(e) if e.code() == ErrorCode::BareRepo => Status::Bare, - Err(e) => return Err(e.into()), - }; - - let url = match repo.find_remote("origin") { - Ok(origin) => origin.url().map(|s| s.to_string()), - Err(e) if e.code() == ErrorCode::NotFound => None, - Err(e) => return Err(Error::Git2Rs(e)), - }; - let email = match include_email { - true => get_email(&repo), - false => None, - }; - debug!( - "generating report for repository at {:?} on branch {} with status {:?}, url {:?}, and email {:?}", - &repo_path, &branch, &status, &url, &email - ); - - Report::new(repo_path, branch, &status, url, email) -} - -/// Checks if local commit(s) on the current branch have not yet been pushed to the remote. -fn is_unpushed(repo: &Repository, head: &Reference) -> Result<bool> { - let local_head = head.peel_to_commit()?; - let remote = format!( - "origin/{}", - match head.shorthand() { - Some(v) => v, - None => { - trace!("assuming unpushed; could not determine shorthand for head"); - return Ok(true); - } - } - ); - let remote_head = repo - .resolve_reference_from_short_name(&remote)? - .peel_to_commit()?; - Ok( - matches!(repo.graph_ahead_behind(local_head.id(), remote_head.id()), Ok(number_unique_commits) if number_unique_commits.0 > 0), - ) -} - -/// Find the "user.email" value in the local or global Git config. The -/// [`git2::Repository::config()`] method will look for a local config first and fallback to -/// global, as needed. Absorb and log any and all errors as the email field is non-critical to -/// the final results. -fn get_email(repository: &Repository) -> Option<String> { - let config = match repository.config() { - Ok(v) => v, - Err(e) => { - trace!("ignored error: {}", e); - return None; - } - }; - let entries = match config.entries(None) { - Ok(v) => v, - Err(e) => { - trace!("ignored error: {}", e); - return None; - } - }; - - // Greedily find our "user.email" value. Return the first result found. - for entry in &entries { - match entry { - Ok(entry) => { - if let Some(name) = entry.name() { - if name == "user.email" { - if let Some(value) = entry.value() { - return Some(value.to_string()); - } - } - } - } - Err(e) => debug!("ignored error: {}", e), - } - } - None -} diff --git a/src/result.rs b/src/result.rs index 8f1d80a..691e240 100644 --- a/src/result.rs +++ b/src/result.rs @@ -1,6 +1,6 @@ -//! This module contains the [`crate::result::Result`] type. +//! This module contains the [`anyhow::Result`] type. use crate::error::Error; -/// Generic [`std::result::Result`] wrapper around [`Error`]. -pub type Result<T> = std::result::Result<T, Error>; +/// Generic [`anyhow::Result`] wrapper around [`Error`]. +pub type Result<T> = anyhow::Result<T, Error>; @@ -1,7 +1,7 @@ //! This module contains the execution logic for generating reports and displaying them to `stdout`. use crate::config::Config; -use crate::result::Result; +use anyhow::Result; use crate::{display, report}; /// This function is the primary entrypoint for the crate. It takes a given config and performs diff --git a/src/target.rs b/src/target.rs deleted file mode 100644 index 3d5383c..0000000 --- a/src/target.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! This module contains target generation logic for eventually generating reports. - -use log::{error, warn}; -use rayon::prelude::*; -use std::fs::DirEntry; -use std::path::{Path, PathBuf}; -use std::{fs, io}; - -/// This type represents bundled target Git directories that were generated from a given [`DirEntry`]. -type Targets = io::Result<Option<Vec<PathBuf>>>; - -/// Ensure the entry is a directory and is not hidden. Then, check if a Git sub directory exists, -/// which will indicate if the entry is a repository. Finally, generate targets based on that -/// repository. -fn process_entry(entry: &DirEntry) -> Targets { - match entry.file_type()?.is_dir() - && !entry - .file_name() - .to_str() - .map(|file_name| file_name.starts_with('.')) - .unwrap_or(false) - { - true => { - let path = entry.path(); - let git_sub_directory = path.join(".git"); - match git_sub_directory.exists() && git_sub_directory.is_dir() { - true => Ok(Some(vec![path])), - false => Ok(Some(recursive_target_gen(&path)?)), - } - } - false => Ok(None), - } -} - -/// Recursive function for generating targets in a child directory. -pub fn recursive_target_gen(path: &Path) -> io::Result<Vec<PathBuf>> { - let entries: Vec<DirEntry> = match fs::read_dir(&path) { - Ok(o) => o.filter_map(|r| r.ok()).collect(), - Err(e) if e.kind() == io::ErrorKind::PermissionDenied => { - warn!("{}: {}", e, &path.display()); - return Ok(vec![]); - } - Err(e) => { - error!("{}: {}", e, &path.display()); - return Ok(vec![]); - } - }; - - let targets = entries - .par_iter() - .map(process_entry) - .collect::<Vec<Targets>>(); - - let mut results = vec![]; - for target in targets { - match target { - Ok(v) => { - if let Some(mut v) = v { - if !v.is_empty() { - results.append(&mut v); - } - } - } - Err(e) => return Err(e), - } - } - Ok(results) -} |