diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2022-04-16 22:34:08 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-16 22:34:08 +0000 |
commit | 4516907acec09ace341927c89d93d1f23c662e92 (patch) | |
tree | 448b3b7710e6959bbd822ec0b9904c0c01eabfb3 | |
parent | df24f0d80db4ef3b4757d6a3fea6be344eae45fc (diff) | |
parent | 468e71e137833d589baa0a651599feb4258f1e84 (diff) | |
download | gfold-4516907acec09ace341927c89d93d1f23c662e92.zip |
Merge #191
191: Fix origin remote and config file lookup errors r=nickgerace a=nickgerace
- Fix error when remote is not "origin" and fallback to alternate remotes if
"origin" is not found
- Add failure scenario to core integration test
- Fix error where "gfold.toml" not existing would result in execution
failure
- Add config file lookup behavior to README
- Remove debug flag in favor of RUST_LOG
- This also results in the logging module being removed since setting
the logging should be configured via RUST_LOG
- Add RUST_LOG and RUST_BACKTRACE to README and CLI help troubleshooting
sections
- Update display flags in README to use `-d`
- Update flag description for color to use "never" instead of "off" as
one of its options
- Add sequencing debug logs (e.g. "foo has completed")
- Switch back to anyhow for its backtrace feature
- Update to 4.0.0-rc.2
Closes #189
Closes #190
Closes #192
Co-authored-by: Nick Gerace <nickagerace@gmail.com>
-rw-r--r-- | CHANGELOG.md | 8 | ||||
-rw-r--r-- | Cargo.lock | 147 | ||||
-rw-r--r-- | Cargo.toml | 6 | ||||
-rw-r--r-- | README.md | 64 | ||||
-rw-r--r-- | docs/DEVELOPING.md | 4 | ||||
-rw-r--r-- | docs/RELEASE.md | 2 | ||||
-rw-r--r-- | scripts/bench-loosely/src/main.rs | 15 | ||||
-rw-r--r-- | src/cli.rs | 47 | ||||
-rw-r--r-- | src/cli/logging.rs | 17 | ||||
-rw-r--r-- | src/config.rs | 27 | ||||
-rw-r--r-- | src/display.rs | 8 | ||||
-rw-r--r-- | src/error.rs | 11 | ||||
-rw-r--r-- | src/main.rs | 126 | ||||
-rw-r--r-- | src/report.rs | 66 | ||||
-rw-r--r-- | src/report/target.rs | 7 | ||||
-rw-r--r-- | src/result.rs | 6 | ||||
-rw-r--r-- | src/run.rs | 2 |
17 files changed, 326 insertions, 237 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index ce4a116..4b279a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,14 +17,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - [git2-rs](https://github.com/rust-lang/git2-rs), which replaces `git` subcommand usage - Even though `git` subcommands were used over **git2-rs** to reduce binary size, significant speed increases could only be achieved by using the latter. - JSON output flag for both version and results printing +- Troubleshooting section to CLI help +- Troubleshooting section to README for using `RUST_LOG` and `RUST_BACKTRACE` ### Changed - Config file location from `<prefix>/gfold/gfold.json` to `<prefix>/gfold.toml` - Config file type from JSON to TOML +- CLI help sections to be divided by headers - Major performance improvements due to moving from sequential target generation to nested, parallel iterators for target generation - Module layout - - `cli`, `display`, and `report` modules now contain their children: `logging`, `color`, and `target` respectively. + - `display` now contains its child, `color` + - `report` now contains its child, `target` - `target` is a new submodule of `display` since they are within the same bounded context, but the former is a subdomain of the latter. - `color` now uses a harness rather than individual functions. - Grey color default to avoid a bug where the `stdout` color is not refreshed within `tmux` when using macOS `Terminal.app` @@ -33,7 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Removed -- `anyhow` dependency (reduced binary size) in favor of internal `Result` type +- Debug flag in favor of using `RUST_LOG` - Display of `none` fields for the standard (default) display of result (i.e. now, if an optional field was not found, it is not shown) - Git path option for CLI and config file - `git` subcommand usage @@ -3,12 +3,27 @@ version = 3 [[package]] -name = "ansi_term" -version = "0.12.1" +name = "addr2line" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" dependencies = [ - "winapi", + "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]] @@ -58,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" @@ -124,22 +154,6 @@ dependencies = [ ] [[package]] -name = "ctor" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "diff" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" - -[[package]] name = "dirs" version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -199,14 +213,14 @@ dependencies = [ [[package]] name = "gfold" -version = "4.0.0-rc.1" +version = "4.0.0-rc.2" dependencies = [ + "anyhow", "argh", "dirs", "env_logger", "git2", "log", - "pretty_assertions", "rayon", "serde", "serde_json", @@ -216,6 +230,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" @@ -286,9 +306,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.121" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" +checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd" [[package]] name = "libgit2-sys" @@ -330,6 +350,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" @@ -339,6 +365,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" @@ -349,12 +385,12 @@ dependencies = [ ] [[package]] -name = "output_vt100" -version = "0.1.3" +name = "object" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" +checksum = "67ac1d3f9a1d3616fd9a60c8d74296f22406a238b6a72f5cc1e6f314df4ffbf9" dependencies = [ - "winapi", + "memchr", ] [[package]] @@ -365,45 +401,33 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pkg-config" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" - -[[package]] -name = "pretty_assertions" -version = "1.2.0" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c038cb5319b9c704bf9c227c261d275bfec0ad438118a2787ce47944fb228b" -dependencies = [ - "ansi_term", - "ctor", - "diff", - "output_vt100", -] +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] name = "proc-macro2" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" dependencies = [ "unicode-xid", ] [[package]] name = "quote" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632d02bff7f874a36f33ea8bb416cd484b90cc66c1194b1a1110d067a7013f58" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" dependencies = [ "proc-macro2", ] [[package]] name = "rayon" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +checksum = "fd249e82c21598a9a426a4e00dd7adc1d640b22445ec8545feef801d1a74c221" dependencies = [ "autocfg", "crossbeam-deque", @@ -413,31 +437,30 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +checksum = "9f51245e1e62e1f1629cbfec37b5793bbabcaeb90f30e94d2ba03564687353e4" dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "lazy_static", "num_cpus", ] [[package]] name = "redox_syscall" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae183fc1b06c149f0c1793e1eb447c8b04bfe46d48e9e48bfb8d2d7ed64ecf0" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" dependencies = [ "bitflags", ] [[package]] name = "redox_users" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7776223e2696f1aa4c6b0170e83212f47296a00424305117d013dfe86fb0fe55" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom", "redox_syscall", @@ -445,6 +468,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" @@ -489,9 +518,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.90" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704df27628939572cd88d33f171cd6f896f4eaca85252c6e0a72d8d8287ee86f" +checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" dependencies = [ "proc-macro2", "quote", @@ -544,9 +573,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "toml" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ "serde", ] @@ -10,9 +10,10 @@ readme = "README.md" repository = "https://github.com/nickgerace/gfold/" edition = "2021" -version = "4.0.0-rc.1" +version = "4.0.0-rc.2" [dependencies] +anyhow = { version = "1", features = ["backtrace"] } argh = "0" dirs = "4" git2 = { version = "0", default_features = false } @@ -28,9 +29,6 @@ toml = "0" # Removed features: ["regex", "termcolor"] env_logger = { version = "0", features = ["atty", "humantime"], default_features = false } -[dev-dependencies] -pretty_assertions = "1" - [profile.release] codegen-units = 1 @@ -34,10 +34,11 @@ pam ~ /home/neloth/src/pam neloth@solstheimcommunityserver.org ``` -The classic display mode can be toggled on with `--classic`. +Want the classic display mode? +Use `-d classic`. ``` -% gfold --classic +% gfold -d classic astrid unclean main git@github.com:db/astrid.git fev bare main none gb unpushed dev https://github.com/hrothgar/gb.git @@ -69,7 +70,7 @@ Analysis is performed by leveraging the [git2-rs](https://github.com/rust-lang/g Pass in `--help` flag to see all the options for using this application. -```bash +```shell gfold gfold .. gfold $HOME @@ -86,27 +87,35 @@ Upon execution, `gfold` will look for a config file at the following path on mac $HOME/.config/gfold.toml ``` -On Windows, the config file is located at the following path: +On Windows, the lookup path will be in a similar location. ```powershell {FOLDERID_Profile}\.config\gfold.toml ``` -Creating and using the config file is entirely optional, and you can ignore your config file at any time using the `-i` flag. +Creating and using the config file is entirely optional. -Here is an example creation workflow for a config file: +For config file creation, you can use the `--dry-run` flag to print valid TOML. +Here is an example config file creation workflow on macOS, Linux and similar platforms: ```shell -gfold --classic ~/ --print > $HOME/.config/gfold.toml +gfold -d classic -c never ~/ --dry-run > $HOME/.config/gfold.toml ``` -This config file will default to the classic display mode and set the default path to `$HOME`, rather than the current working directory. - Here are the contents of the resulting config file: ```toml path = '/home/neloth' display_mode = 'Classic' +color_mode = 'Never' +``` + +Let's say you created a config file, but wish to execute `gfold` with entirely different settings _and_ you want to ensure that +you do not accidentally inherit options from the config file. +In that scenario you can ignore your config file by using the `-i` flag. + +```shell +gfold -i ``` You can back up a config file and track its history with `git`. @@ -191,6 +200,28 @@ The uninstall script can also be used for cleanup in the event of a failed insta **Preferred package manager not listed:** please [file an issue](https://github.com/nickgerace/gfold/issues/new/choose)! +## Compatibility + +`gfold` is intended to be ran on *any* tier one Rust 🦀 target. +Please [file an issue](https://github.com/nickgerace/gfold/issues) if your platform is unsupported. + +## Troubleshooting + +If you encounter unexpected behavior or a bug, please [file an issue](https://github.com/nickgerace/gfold/issues) and debug +locally with `RUST_BACKTRACE=1 RUST_LOG=debug` prepended when executing `gfold`. +You can also adjust each variable, as needed, to aid investigation. +Please attach relevant logs from execution with sensitive bits redacted in order to help resolve your issue. + +### Coreutils Collision on macOS + +If `fold` from [GNU Coreutils](https://www.gnu.org/software/coreutils/) is installed on macOS via `brew`, it will be named `gfold`. +You can avoid this collision with shell aliases, shell functions, and/or `PATH` changes. +Here is an example with the `o` dropped from `gfold`: + +```shell +alias gfld=$HOME/.cargo/bin/gfold +``` + ## Community For more information and thanks to contributors, users, and the "community" at large, please refer to the **[THANKS](./docs/THANKS.md)** file. @@ -209,18 +240,3 @@ For more information and thanks to contributors, users, and the "community" at l - [nixpkgs](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/version-management/git-and-tools/gfold/default.nix) for the `gfold` package - [AUR](https://github.com/orhun/PKGBUILDs) for the `gfold-git` (VCS/development) package - In the past, this included the `gfold` and `gfold-bin` packages as well, they those have been deprecated in favor of the official community repository package above - -## Compatibility - -`gfold` is intended to be ran on *any* tier one Rust 🦀 target. -Please [file an issue](https://github.com/nickgerace/gfold/issues) if your platform is unsupported. - -## Troubleshooting - -If `fold` from [GNU Coreutils](https://www.gnu.org/software/coreutils/) is installed on macOS via `brew`, it will be named `gfold`. -You can avoid this collision with shell aliases, shell functions, and/or `PATH` changes. -Here is an example with the `o` dropped from `gfold`: - -```shell -alias gfld=$HOME/.cargo/bin/gfold -``` diff --git a/docs/DEVELOPING.md b/docs/DEVELOPING.md index d990684..23b6607 100644 --- a/docs/DEVELOPING.md +++ b/docs/DEVELOPING.md @@ -18,9 +18,9 @@ Now, ensure that lints, tests, and builds succeed. ```shell cargo fmt --all -- --check cargo clippy -- -D warnings -cargo doc +cargo doc --all cargo test -cargo build +cargo build --all-targets ``` > Alternatively, you can replace `cargo test` above with [cargo nextest](https://github.com/nextest-rs/nextest). diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 3c0f43e..7d177c5 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -23,7 +23,7 @@ cargo build - [ ] Create and _do not merge_ a commit with the following message: `Update to <tag>` - [ ] Test and verify the publishing workflow: -```bash +```shell cargo publish --dry-run ``` diff --git a/scripts/bench-loosely/src/main.rs b/scripts/bench-loosely/src/main.rs index 68d58f6..37da22d 100644 --- a/scripts/bench-loosely/src/main.rs +++ b/scripts/bench-loosely/src/main.rs @@ -1,7 +1,4 @@ -use std::fs; -use std::fs::Metadata; -use std::io; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process::Command; use std::time::{Duration, Instant}; @@ -55,12 +52,10 @@ fn main() { fn loose_bench(new: &Path, old: &Path, target: &Path) { let new_duration = execute(new, target); let old_duration = execute(old, target); - let (new_text, old_text) = if new_duration > old_duration { - ("LOST", "WON ") - } else if new_duration < old_duration { - ("WON ", "LOST") - } else { - ("TIE ", "TIE ") + let (new_text, old_text) = match new_duration { + new_duration if new_duration > old_duration => ("LOST", "WON "), + new_duration if new_duration < old_duration => ("WON ", "LOST"), + _ => ("TIE ", "TIE "), }; println!( @@ -3,26 +3,32 @@ use crate::config::{ColorMode, Config, DisplayMode}; use crate::error::Error; -use crate::result::Result; use crate::run; +use anyhow::Result; use argh::FromArgs; +use log::debug; use std::env; -mod logging; - #[derive(FromArgs)] #[argh(description = "More information: https://github.com/nickgerace/gfold -This application helps you keep track of multiple Git repositories via CLI. -By default, it displays relevant information for all repos in the current -working directory. +Description: + This application helps you keep track of multiple Git repositories via CLI. + By default, it displays relevant information for all repos in the current + working directory. + +Config File Usage: + While CLI options are prioritized, default options will fallback to the + config file if it exists. Here is the config file lookup locations for some + common platforms: -While CLI options are prioritized, default options will fallback to the config -file if it exists. Here is the config file lookup locations for some common -operating systems: + macOS, Linux, etc. $HOME/.config/gfold.toml + Windows {{FOLDERID_Profile}}\\.config\\gfold.toml - macOS/Linux $HOME/.config/gfold.toml - Windows {{FOLDERID_Profile}}\\.config\\gfold.toml")] +Troubleshooting: + Investigate unexpected behavior by prepending execution with + \"RUST_BACKTRACE=1\"and \"RUST_LOG=debug\". You can adjust those variable's + values to aid investigation.")] struct Args { #[argh( positional, @@ -33,23 +39,18 @@ struct Args { #[argh( option, short = 'c', - description = "specify color mode [options: \"always\", \"compatibility\", \"off\"]" + description = "specify color mode [options: \"always\", \"compatibility\", \"never\"]" )] color_mode: Option<String>, #[argh( - switch, - description = "enable debug logging (sets \"RUST_LOG\" to \"debug\")" - )] - debug: bool, - #[argh( option, short = 'd', - description = "specify display format [options: \"standard\"/\"default\", \"json\", \"classic\"]" + description = "specify display format [options: \"standard\" or \"default\", \"json\", \"classic\"]" )] display_mode: Option<String>, #[argh( switch, - description = "display config options chosen, including those from the config file if they exist" + description = "display finalized config options and exit (merged options from an optional config file and command line arguments)" )] dry_run: bool, #[argh(switch, short = 'i', description = "ignore config file settings")] @@ -68,19 +69,20 @@ pub fn parse_and_run() -> Result<()> { // First and foremost, get logging up and running. We want logs as quickly as possible for // debugging by setting "RUST_LOG". let args: Args = argh::from_env(); - logging::init(args.debug); + debug!("collected args"); let mut config = match args.ignore_config_file { true => Config::new()?, false => Config::try_config()?, }; + debug!("loaded initial config"); if let Some(found_display_mode) = &args.display_mode { config.display_mode = match found_display_mode.to_lowercase().as_str() { "classic" => DisplayMode::Classic, "json" => DisplayMode::Json, "standard" | "default" => DisplayMode::Standard, - _ => return Err(Error::InvalidDisplayMode(found_display_mode.to_string())), + _ => return Err(Error::InvalidDisplayMode(found_display_mode.to_string()).into()), } } @@ -99,7 +101,7 @@ pub fn parse_and_run() -> Result<()> { "always" => ColorMode::Always, "compatibility" => ColorMode::Compatibility, "never" => ColorMode::Never, - _ => return Err(Error::InvalidColorMode(found_color_mode.to_string())), + _ => return Err(Error::InvalidColorMode(found_color_mode.to_string()).into()), } } @@ -107,6 +109,7 @@ pub fn parse_and_run() -> Result<()> { config.path = env::current_dir()?.join(found_path).canonicalize()?; } + debug!("finalized config options"); match &args.dry_run { true => config.print(), false => run::run(&config), diff --git a/src/cli/logging.rs b/src/cli/logging.rs deleted file mode 100644 index f0f1ac6..0000000 --- a/src/cli/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/config.rs b/src/config.rs index f898d55..db3537a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,10 +1,10 @@ //! 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; +use std::{env, fs, io}; /// This struct is the actual config type consumed through the codebase. It is boostrapped via its /// public methods and uses [`EntryConfig`], a private struct, under the hood in order to @@ -65,29 +65,34 @@ pub enum ColorMode { impl Config { /// This method tries to deserialize the config file (empty, non-existent, partial or complete) - /// and uses "EntryConfig" as an intermediary struct. This is the primary method used when + /// and uses [`EntryConfig`] as an intermediary struct. This is the primary method used when /// creating a config. pub fn try_config() -> Result<Self> { // Within this method, we check if the config file is empty before deserializing it. Users - // should be able to proceed with empty config files. If empty, then we fall back to the - // "EntryConfig" default before conversion. + // should be able to proceed with empty config files. If empty or not found, then we fall + // back to the "EntryConfig" default before conversion. let home = dirs::home_dir().ok_or(Error::HomeDirNotFound)?; let path = home.join(".config").join("gfold.toml"); - let contents = std::fs::read_to_string(path)?; - let entry_config = match contents.is_empty() { - true => EntryConfig::default(), - false => toml::from_str(&contents)?, + let entry_config = match fs::read_to_string(path) { + Ok(contents) => match contents.is_empty() { + true => EntryConfig::default(), + false => toml::from_str(&contents)?, + }, + Err(e) => match e.kind() { + io::ErrorKind::NotFound => EntryConfig::default(), + _ => return Err(e.into()), + }, }; Self::from_entry_config(&entry_config) } - /// This method does not look for the config file and uses "EntryConfig"'s defaults instead. + /// This method does not look for the config file and uses [`EntryConfig`]'s defaults instead. /// It is best for testing use and when the user wishes to skip config file lookup. pub fn new() -> Result<Self> { Self::from_entry_config(&EntryConfig::default()) } - // This method prints the full config (merged with config file, as needed) as valid, pretty TOML. + /// This method prints the full config (merged with config file, as needed) as valid, pretty TOML. pub fn print(self) -> Result<()> { print!("{}", toml::to_string_pretty(&self)?); Ok(()) diff --git a/src/display.rs b/src/display.rs index fc1a339..6440021 100644 --- a/src/display.rs +++ b/src/display.rs @@ -4,7 +4,8 @@ use crate::config::{ColorMode, DisplayMode}; use crate::display::color::ColorHarness; use crate::error::Error; use crate::report::LabeledReports; -use crate::result::Result; +use anyhow::Result; +use log::debug; use log::warn; use std::path::Path; @@ -21,13 +22,14 @@ pub fn display( ) -> Result<()> { match display_mode { DisplayMode::Standard => standard(reports, color_mode), - DisplayMode::Json => Ok(json(reports)?), + DisplayMode::Json => json(reports), DisplayMode::Classic => classic(reports, color_mode), } } /// Display [`LabeledReports`] to `stdout` in the standard (default) format. fn standard(reports: &LabeledReports, color_mode: &ColorMode) -> Result<()> { + debug!("detected standard display mode"); let mut all_reports = Vec::new(); for grouped_report in reports { all_reports.append(&mut grouped_report.1.clone()); @@ -71,6 +73,7 @@ fn standard(reports: &LabeledReports, color_mode: &ColorMode) -> Result<()> { /// Display [`LabeledReports`] to `stdout` in JSON format. fn json(reports: &LabeledReports) -> Result<()> { + debug!("detected json display mode"); let mut all_reports = Vec::new(); for grouped_report in reports { all_reports.append(&mut grouped_report.1.clone()); @@ -83,6 +86,7 @@ fn json(reports: &LabeledReports) -> Result<()> { /// Display [`LabeledReports`] to `stdout` in the classic format. fn classic(reports: &LabeledReports, color_mode: &ColorMode) -> Result<()> { + debug!("detected classic display mode"); let color_harness = ColorHarness::new(color_mode); let length = reports.keys().len(); diff --git a/src/error.rs b/src/error.rs index 09fc0d2..6eeda29 100644 --- a/src/error.rs +++ b/src/error.rs @@ -21,15 +21,4 @@ pub enum Error { GitReferenceShorthandInvalid, #[error("could not find home directory")] HomeDirNotFound, - - #[error("git2::Error")] - Git2Rs(#[from] git2::Error), - #[error("serde_json::Error")] - SerdeJson(#[from] serde_json::Error), - #[error("std::io::Error")] - StdIo(#[from] std::io::Error), - #[error("toml::de::Error")] - TomlDe(#[from] toml::de::Error), - #[error("toml::ser::Error")] - TomlSe(#[from] toml::ser::Error), } diff --git a/src/main.rs b/src/main.rs index ea39e87..f1c7c72 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,61 +2,48 @@ //! 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; +use env_logger::Builder; +use log::debug; +use log::LevelFilter; +use std::env; mod cli; mod config; mod display; mod error; mod report; -mod result; mod run; mod status; -/// Calls [`cli::parse_and_run()`] to generate a [`config::Config`] and eventually call [`run::run()`]; +/// Initializes the logger based on the debug flag and `RUST_LOG` environment variable and calls +/// [`cli::parse_and_run()`] to generate a [`config::Config`] and eventually call [`run::run()`]. fn main() -> Result<()> { - cli::parse_and_run() + match env::var("RUST_LOG").is_err() { + true => Builder::new().filter_level(LevelFilter::Off).init(), + false => env_logger::init(), + } + debug!("initialized logger"); + + cli::parse_and_run()?; + Ok(()) } #[cfg(test)] mod tests { use super::*; - use crate::config::{Config, DisplayMode}; + use crate::config::{ColorMode, Config, DisplayMode}; use crate::report::{LabeledReports, Report}; use crate::status::Status; + use git2::ErrorCode; use git2::Repository; - use pretty_assertions::assert_eq; use std::collections::BTreeMap; - use std::path::Path; + use std::path::{Path, PathBuf}; use std::{env, fs, io}; #[test] fn integration() { - fn create_dir_or_die(path: &Path) { - if let Err(e) = fs::create_dir(path) { - if e.kind() != io::ErrorKind::AlreadyExists { - panic!( - "could not create directory ({:?}) due to error kind: {:?}", - path, - e.kind() - ); - } - } - } - - fn create_file_or_die(path: &Path) { - if let Err(e) = fs::File::create(path) { - if e.kind() != io::ErrorKind::AlreadyExists { - panic!( - "could not create file ({:?}) due to error kind: {:?}", - path, - e.kind() - ); - } - } - } - // Test directory structure within "target": // └── test // ├── bar @@ -69,35 +56,64 @@ mod tests { // ├── three // └── two - let cwd = env::current_dir().expect("failed to get current working directory"); - let target = cwd.join("target"); - create_dir_or_die(&target); + let manifest_directory = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let target = manifest_directory.join("target"); + create_directory(&target); + + // Warning: setting up test directory by removing it and its contents recursively. let test = target.join("test"); - create_dir_or_die(&test); + if let Err(e) = fs::remove_dir_all(&test) { + if e.kind() != io::ErrorKind::NotFound { + panic!( + "could not remove directory and its contents ({:?}) due to error kind: {:?}", + &test, + e.kind() + ); + } + } + create_directory(&test); + for name in ["foo", "bar", "baz"] { let current = test.join(name); - create_dir_or_die(¤t); + create_directory(¤t); Repository::init(¤t).expect("could not initialize repository"); if name == "foo" { - create_file_or_die(¤t.join("newfile")); + create_file(¤t.join("newfile")); } } let nested = test.join("nested"); - create_dir_or_die(&nested); + create_directory(&nested); for name in ["one", "two", "three"] { let current = nested.join(name); - create_dir_or_die(¤t); - Repository::init(¤t).expect("could not initialize repository"); + create_directory(¤t); + let repository = Repository::init(¤t).expect("could not initialize repository"); if name == "one" { - create_file_or_die(¤t.join("newfile")); + create_file(¤t.join("newfile")); + } + + if name == "two" { + if let Err(e) = repository.remote("origin", "https://github.com/nickgerace/gfold") { + if e.code() != ErrorCode::Exists { + panic!("{}", e); + } + } + } + + if name == "three" { + if let Err(e) = repository.remote("fork", "https://github.com/nickgerace/gfold") { + if e.code() != ErrorCode::Exists { + panic!("{}", e); + } + } } } let mut config = Config::new().expect("could not create new config"); config.path = test; + config.color_mode = ColorMode::Never; assert!(run::run(&config).is_ok()); // Now, let's ensure our reports are what we expect. @@ -139,7 +155,7 @@ mod tests { &nested_test_dir.join("two"), "HEAD", &Status::Clean, - None, + Some("https://github.com/nickgerace/gfold".to_string()), None, ) .expect("could not create report"), @@ -147,7 +163,7 @@ mod tests { &nested_test_dir.join("three"), "HEAD", &Status::Clean, - None, + Some("https://github.com/nickgerace/gfold".to_string()), None, ) .expect("could not create report"), @@ -168,4 +184,28 @@ mod tests { assert_eq!(found_labeled_reports_sorted, expected_reports); } + + fn create_directory(path: &Path) { + if let Err(e) = fs::create_dir(path) { + if e.kind() != io::ErrorKind::AlreadyExists { + panic!( + "could not create directory ({:?}) due to error kind: {:?}", + path, + e.kind() + ); + } + } + } + + fn create_file(path: &Path) { + if let Err(e) = fs::File::create(path) { + if e.kind() != io::ErrorKind::AlreadyExists { + panic!( + "could not create file ({:?}) due to error kind: {:?}", + path, + e.kind() + ); + } + } + } } diff --git a/src/report.rs b/src/report.rs index bcf0dbb..70a2799 100644 --- a/src/report.rs +++ b/src/report.rs @@ -2,9 +2,9 @@ use crate::config::DisplayMode; use crate::error::Error; -use crate::result::Result; use crate::status::Status; -use git2::{ErrorCode, Reference, Repository, StatusOptions}; +use anyhow::Result; +use git2::{ErrorCode, Reference, Remote, Repository, StatusOptions}; use log::{debug, trace}; use rayon::prelude::*; use serde::{Deserialize, Serialize}; @@ -52,16 +52,18 @@ impl Report { 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::FileNameToStrConversionFailure(path.to_path_buf()).into()) + } }, - None => return Err(Error::FileNameNotFound(path.to_path_buf())), + None => return Err(Error::FileNameNotFound(path.to_path_buf()).into()), }, 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 => return Err(Error::PathToStrConversionFailure(s.to_path_buf()).into()), }, None => None, }, @@ -101,14 +103,17 @@ pub fn generate_reports(path: &Path, display_mode: &DisplayMode) -> Result<Label /// 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); + debug!( + "attemping to generate report for repository at 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)), + Err(e) => return Err(e.into()), }; let branch = match &head { Some(head) => head @@ -117,6 +122,17 @@ fn generate_report(repo_path: &Path, include_email: bool) -> Result<Report> { None => HEAD, }; + // Greedily chooses a remote if "origin" is not found. + let (remote, remote_name) = match repo.find_remote("origin") { + Ok(origin) => (Some(origin), Some("origin".to_string())), + Err(e) if e.code() == ErrorCode::NotFound => choose_remote_greedily(&repo)?, + Err(e) => return Err(e.into()), + }; + let url = match remote { + Some(remote) => remote.url().map(|s| s.to_string()), + None => None, + }; + // 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); @@ -125,9 +141,12 @@ fn generate_report(repo_path: &Path, include_email: bool) -> Result<Report> { // 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, + Some(head) => match remote_name { + Some(remote_name) => match is_unpushed(&repo, head, &remote_name)? { + true => Status::Unpushed, + false => Status::Clean, + }, + None => Status::Clean, }, None => Status::Clean, }, @@ -136,28 +155,24 @@ fn generate_report(repo_path: &Path, include_email: bool) -> Result<Report> { 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 + "finalized report collection for repository at path: {:?}", + repo_path ); - 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> { +fn is_unpushed(repo: &Repository, head: &Reference, remote_name: &str) -> Result<bool> { let local_head = head.peel_to_commit()?; let remote = format!( - "origin/{}", + "{}/{}", + remote_name, match head.shorthand() { Some(v) => v, None => { @@ -211,3 +226,14 @@ fn get_email(repository: &Repository) -> Option<String> { } None } + +fn choose_remote_greedily(repository: &Repository) -> Result<(Option<Remote>, Option<String>)> { + let remotes = repository.remotes()?; + Ok(match remotes.get(0) { + Some(remote_name) => ( + Some(repository.find_remote(remote_name)?), + Some(remote_name.to_string()), + ), + None => (None, None), + }) +} diff --git a/src/report/target.rs b/src/report/target.rs index 3d5383c..3401101 100644 --- a/src/report/target.rs +++ b/src/report/target.rs @@ -1,6 +1,6 @@ //! This module contains target generation logic for eventually generating reports. -use log::{error, warn}; +use log::{debug, error, warn}; use rayon::prelude::*; use std::fs::DirEntry; use std::path::{Path, PathBuf}; @@ -24,7 +24,10 @@ fn process_entry(entry: &DirEntry) -> Targets { 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])), + true => { + debug!("found target: {:?}", &path); + Ok(Some(vec![path])) + } false => Ok(Some(recursive_target_gen(&path)?)), } } diff --git a/src/result.rs b/src/result.rs deleted file mode 100644 index 8f1d80a..0000000 --- a/src/result.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! This module contains the [`crate::result::Result`] type. - -use crate::error::Error; - -/// Generic [`std::result::Result`] wrapper around [`Error`]. -pub type Result<T> = std::result::Result<T, Error>; @@ -1,8 +1,8 @@ //! This module contains the execution logic for generating reports and displaying them to `stdout`. use crate::config::Config; -use crate::result::Result; use crate::{display, report}; +use anyhow::Result; /// This function is the primary entrypoint for the crate. It takes a given config and performs /// the end-to-end workflow using it. At this point, all CLI and config file options should be |