summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Gerace <nickagerace@gmail.com>2022-03-25 17:42:40 -0400
committerNick Gerace <nickagerace@gmail.com>2022-03-25 17:44:40 -0400
commitbcd37c3e8f3b13510a7947327e7723e56c1a1284 (patch)
treeb530858ad7d883d05155e7d1211d4a5bd336c187
parent7ea0a538cbc2924de7c0baf12222767a3e1ace48 (diff)
downloadgfold-bcd37c3e8f3b13510a7947327e7723e56c1a1284.zip
Revert "Revert accidental WIP commit"
This reverts commit 7ea0a538cbc2924de7c0baf12222767a3e1ace48.
-rw-r--r--Cargo.lock77
-rw-r--r--Cargo.toml1
-rw-r--r--src/color.rs74
-rw-r--r--src/config.rs2
-rw-r--r--src/logging.rs17
-rw-r--r--src/main.rs25
-rw-r--r--src/mod.rs213
-rw-r--r--src/result.rs6
-rw-r--r--src/run.rs2
-rw-r--r--src/target.rs68
10 files changed, 105 insertions, 380 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 6b83455..c8cf843 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index d9b8ecc..c3cb024 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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(&current);
- Repository::init(&current).expect("could not initialize repository");
+ let repository = Repository::init(&current).expect("could not initialize repository");
if name == "one" {
create_file_or_die(&current.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>;
diff --git a/src/run.rs b/src/run.rs
index 8c25e09..bcc0195 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -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)
-}