diff options
author | Nick Gerace <nickagerace@gmail.com> | 2021-12-24 16:38:03 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-12-24 16:38:03 -0500 |
commit | 975fe5b902798e1b357a9acf3245c05b83b2ffab (patch) | |
tree | cac8368482556d664c5b1317d6fcdff39837f1f8 | |
parent | f7c11ab13880c5ff6b5e8b53e524b037ec7af465 (diff) | |
download | gfold-975fe5b902798e1b357a9acf3245c05b83b2ffab.zip |
Refactor config to be the only public config type (#153)
- Refactor config to ensure entry config is wrapped by private functions
and methods
- Ensure tests skip config file with new functionality, which relies on
entry config using defaults
- Update issue and PR templates to be more clear in what they ask from
users (cleaner checklists)
- Switch RELEASE to use a list instead an emoji-based markdown table
- Push RELEASE commands into their own code blocks
- Add new emoji-based "logging" headers to uninstall/install scripts
- Refactor uninstall/install scripts to use functions for readability
- Add ignore config file option
- Give every major type, function, and method a comment-based
description
- Fix issue template by adding back name and about
- Use get flag for remote origin url
Signed-off-by: Nick Gerace <nickagerace@gmail.com>
-rw-r--r-- | .github/ISSUE_TEMPLATE/issue.md | 17 | ||||
-rw-r--r-- | .github/pull_request_template.md | 5 | ||||
-rw-r--r-- | .github/workflows/push.yml | 8 | ||||
-rw-r--r-- | .github/workflows/tag.yml | 1 | ||||
-rw-r--r-- | CHANGELOG.md | 11 | ||||
-rw-r--r-- | Cargo.toml | 9 | ||||
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | RELEASE.md | 65 | ||||
-rwxr-xr-x | scripts/install.sh | 59 | ||||
-rwxr-xr-x | scripts/uninstall.sh | 19 | ||||
-rw-r--r-- | src/cli.rs | 13 | ||||
-rw-r--r-- | src/config.rs | 42 | ||||
-rw-r--r-- | src/main.rs | 8 | ||||
-rw-r--r-- | src/report.rs | 21 | ||||
-rw-r--r-- | src/run.rs | 3 |
15 files changed, 197 insertions, 86 deletions
diff --git a/.github/ISSUE_TEMPLATE/issue.md b/.github/ISSUE_TEMPLATE/issue.md index 0bbea7a..af7aaf8 100644 --- a/.github/ISSUE_TEMPLATE/issue.md +++ b/.github/ISSUE_TEMPLATE/issue.md @@ -1,5 +1,12 @@ -<!-- Thanks for filing an issue! Here are some good things to include in order to help get the issue solved quickly: --> -<!-- 1. Providing a clear description of the issue (alongside reproduction steps, as needed) --> -<!-- 2. Providing what OS you are using --> -<!-- 3. Providing the method of installing/obtaining the application --> -<!-- 4. Pasting the output of `gfold -V` to confirm the version --> +--- +name: Issue +about: Create an issue. +--- + +<!-- Thanks for filing an issue! Since this is a volunteer-ran repository, filing out your issue with the following can help get your issue solved quickly: --> + +<!-- 1) A clear description of the issue (alongside reproduction steps, as needed) --> +<!-- 2) What OS you are using --> +<!-- 3) Your method of installing/obtaining the application, if relevant --> +<!-- 4) The output of `gfold -V` (confirm the version based on the `gfold` installation in your `PATH`) --> +<!-- 5) The output of `gfold --debug`, if relevant --> diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b04b6e1..7440c61 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1 +1,4 @@ -<!-- Please describe what your PR does and the reasonining behind it. In addition, please link relevant issue(s), as needed. --> +<!-- Thanks for filing a PR! Since this is a volunteer-ran repository, filing out your PR with the following can help get your PR reviewed quickly: --> + +<!-- 1) Please describe what your PR does and the reasonining behind it. --> +<!-- 2) Please link relevant issue(s), as needed. --> diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 24cf20d..3c98fbd 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -4,11 +4,12 @@ on: branches-exclude: - "main" paths: - - **/*.rs - - **/Cargo.* - - **/rustfmt.toml + - "**.rs" + - "Cargo.*" + - "rustfmt.toml" jobs: + prepare: runs-on: ubuntu-latest steps: @@ -30,6 +31,7 @@ jobs: - uses: actions-rs/cargo@v1 with: command: test + build: runs-on: ${{ matrix.os }} strategy: diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index a538bec..6e840e7 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -4,6 +4,7 @@ on: tags: - "*" jobs: + publish: runs-on: ${{ matrix.os }} strategy: diff --git a/CHANGELOG.md b/CHANGELOG.md index a9c23f2..f740f07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,19 +11,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added +- Ability to ignore config file options +- Ability to print merged config options +- Ability to store default path target in config file (defaults to current working directory) - Ability to use config file in `$HOME/.config/gfold/gfold.json` and `{FOLDERID_Profile}\.config\gfold\gfold.json` -- Experimental new display mode (`--new` flag) +- Ability to use old display mode with `--classic` flag and store preference in config file - Formal CLI parsing library, `argh` - Install and uninstall scripts +- New display mode that avoids grouping repositories (API-breaking since this is the new default display mode) ### Changed -- Codebase to a domain-driven architecture +- Codebase to a domain-driven architecture (major refactor) ### Notes -- This crate has used other CLI parsing libraries in the past, and recently did not use any, but with manual testing and [publicly available benchmarks](https://github.com/rust-cli/argparse-benchmarks-rs/blob/c37e78aabdaa4384a9c49be3735a686803d0e37a/README.md#results), `argh` is now in use. - Evaluated using `tracing` and `tracing-subscriber` over `log` and `env_logger`, but due to their combined larger size, the logging crates remain the same as before. +- The config file can be non-existent, empty, partially filled out or completely filled out. There's also an option to ignore the config file completely and only use CLI options. +- This crate has used other CLI parsing libraries in the past, and recently did not use any, but with manual testing and [publicly available benchmarks](https://github.com/rust-cli/argparse-benchmarks-rs/blob/c37e78aabdaa4384a9c49be3735a686803d0e37a/README.md#results), `argh` is now in use. ## [2.0.2] - 2021-12-02 @@ -2,13 +2,14 @@ authors = ["Nick Gerace <nickagerace@gmail.com>"] categories = ["command-line-utilities", "command-line-interface"] description = "CLI tool to help keep track of your Git repositories." -edition = "2021" homepage = "https://nickgerace.dev" keywords = ["git", "cli"] license = "Apache-2.0" name = "gfold" readme = "README.md" repository = "https://github.com/nickgerace/gfold/" + +edition = "2021" version = "2.0.2" [dependencies] @@ -32,9 +33,9 @@ codegen-units = 1 # Instruct linker to optimize at the link stage. lto = true -# This application should not panic often and only read from the filesystem. -panic = "abort" - # There is a noticeable speed difference from level 3 to 'z' or 's'. # We need this speed for the user experience. opt-level = 3 + +# This application should not panic often and only read from the filesystem. +panic = "abort" @@ -38,7 +38,7 @@ bench-loosely: @echo "=============================================================" @time $(INSTALLED) ~/ @echo "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" - @time $(NEW) ~/ + @time $(NEW) -i ~/ @echo "=============================================================" @du -h $(INSTALLED) @du -h $(NEW) @@ -5,24 +5,53 @@ This document contains all information related to release. ## Checklist This checklist details the `gfold` release process. -Steps should (and often must) be executed in sequential order. - -| RC | Release | Step | -|-----|---------|---------------------------------------------------------------------------------------------------------------------------------| -| ✔️ | ✔️ | Checkout and rebase `main` to its latest commit. | -| ✔️ | ✔️ | Change the `version` field in `Cargo.toml` to the new tag. | -| ⛔ | ✔️ | Change the version in `CHANGELOG.md` and uncomment the line, `<!--The latest version contains all changes.-->`. | -| ✔️ | ✔️ | Run `make ci build` and verify that everything looks/works as expected. | -| ✔️ | ✔️ | Create a commit with the following message: `Update to <tag>`. **Do not push (or merge) the commit yet.** | -| ✔️ | ✔️ | Test and verify the publishing workflow: `cargo publish --dry-run`. | -| ✔️ | ✔️ | Push (or merge) the preparation commit into `main`. | -| ✔️ | ✔️ | Checkout and rebase `main` to its latest commit, which should be the aforementioned commit. | -| ✔️ | ✔️ | Tag with `git tag <tag>` and push the tag: `git push --tags origin main`. | -| ✔️ | ✔️ | Publish the crate: `cargo publish`. | -| ✔️ | ✔️ | Verify that the [crate](https://crates.io/crates/gfold) on `crates.io` looks correct. | -| ✔️ | ✔️ | Download the crate via `cargo install --locked gfold` or `cargo install --locked --version <tag> gfold`. | -| ✔️ | ✔️ | Verify that the [GitHub release](https://github.com/nickgerace/gfold/releases) on the repository's releases page looks correct. | -| ⛔ | ✔️ | Update the formula for the [Hombrew tap](https://github.com/nickgerace/homebrew-nickgerace). | +Steps should (and frequently must) be executed in sequential order. + +- [ ] Checkout and rebase `main` to its latest commit +- [ ] Change the `version` field in `Cargo.toml` to the new tag +- [ ] **Full Releases Only**: change the version in `CHANGELOG.md` and uncomment the following line: `<!--The latest version contains all changes.-->` +- [ ] Run final `make` targets and verify that everything looks/works as expected: + +```bash +make ci build +``` + +- [ ] Create and _do not push/merge_ a commit with the following message: `Update to <tag>` + +- [ ] Test and verify the publishing workflow: + +```bash +cargo publish --dry-run +``` + +- [ ] Push/merge the preparation commit into `main` +- [ ] Checkout and rebase `main` to its latest commit, which should be the aforementioned commit +- [ ] Tag and push the tag: + +```bash +git tag <tag> +git push --tags origin main +``` + +- [ ] Publish the crate: + +```bash +cargo publish +``` + +- [ ] Verify that the [crate](https://crates.io/crates/gfold) on `crates.io` looks correct +- [ ] Download and install the crate: + +```bash +# Full releases +cargo install --locked gfold + +# Release candidates (RCs) +cargo install --locked --version <tag> gfold +``` + +- [ ] Verify that the [GitHub release](https://github.com/nickgerace/gfold/releases) on the repository's releases page looks correct +- [ ] **Full Releases Only**: Update the formula for the [Hombrew tap](https://github.com/nickgerace/homebrew-nickgerace) ## Versioning Scheme diff --git a/scripts/install.sh b/scripts/install.sh index cd13b2f..be0bc7c 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,27 +1,46 @@ #!/usr/bin/env bash set -e -for BINARY in "jq" "wget" "curl"; do - if ! [ "$(command -v ${BINARY})" ]; then - echo "error: \"$BINARY\" must be installed and in PATH" +function check-dependencies { + for BINARY in "jq" "wget" "curl"; do + if ! [ "$(command -v ${BINARY})" ]; then + echo "[install-gfold] 🚫 \"$BINARY\" must be installed and in PATH" + exit 1 + fi + done +} + +function perform-install { + local INSTALL_OS + if [ "$(uname -s)" = "Linux" ] && [ "$(uname -m)" = "x86_64" ]; then + INSTALL_OS="linux-gnu" + elif [ "$(uname -s)" = "Darwin" ] && [ "$(uname -m)" = "x86_64" ]; then + INSTALL_OS="darwin" + else + echo "[install-gfold] 🚫 must execute on Linux or Darwin x86_64 host" + echo "[install-gfold] 🚫 for more installation methods: https://github.com/nickgerace/gfold" exit 1 fi -done -INSTALL_OS="unknown" -if [ "$(uname -s)" = "Linux" ] && [ "$(uname -m)" = "x86_64" ]; then - echo "assuming glibc (GNU) and not another libc (e.g. musl-libc)" - INSTALL_OS="linux-gnu" -elif [ "$(uname -s)" = "Darwin" ] && [ "$(uname -m)" = "x86_64" ]; then - INSTALL_OS="darwin" -else - echo "error: must execute on Linux or Darwin x86_64 host" - exit 1 -fi + LATEST=$(curl -s https://api.github.com/repos/nickgerace/gfold/releases/latest | jq -r ".tag_name") + if [ -f /tmp/gfold ]; then + rm /tmp/gfold + fi + wget -O /tmp/gfold https://github.com/nickgerace/gfold/releases/download/$LATEST/gfold-$INSTALL_OS-amd64 + chmod +x /tmp/gfold + + if [ -f /usr/local/bin/gfold ]; then + rm /usr/local/bin/gfold + fi + mv /tmp/gfold /usr/local/bin/gfold + + echo "[install-gfold] ✅ gfold has been installed to /usr/local/bin/gfold" + if [ $INSTALL_OS = "linux-gnu" ]; then + echo "[install-gfold] ⚠️ assuming glibc (GNU) and not another libc (e.g. musl-libc)" + echo "[install-gfold] ⚠️ if using another libc, you may need to choose another installation method" + echo "[install-gfold] ⚠️ for more information: https://github.com/nickgerace/gfold" + fi +} -LATEST=$(curl -s https://api.github.com/repos/nickgerace/gfold/releases/latest | jq -r ".tag_name") -if [ -f /tmp/gfold ]; then rm /tmp/gfold; fi -wget -O /tmp/gfold https://github.com/nickgerace/gfold/releases/download/$LATEST/gfold-$INSTALL_OS-amd64 -chmod +x /tmp/gfold -mv /tmp/gfold /usr/local/bin/gfold -echo "gfold has been installed to /usr/local/bin/gfold" +check-dependencies +perform-install
\ No newline at end of file diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index d6e683a..3064282 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -1,12 +1,19 @@ #!/usr/bin/env bash set -e -function delete-binary { - if [ -f "$1" ]; then - rm "$1" +function perform-uninstall { + for FILE in "/tmp/gfold" "/usr/local/bin/gfold"; do + if [ -f "$FILE" ]; then + rm "$FILE" + echo "[uninstall-gfold] ✅ deleted $FILE" + fi + done + echo "[uninstall-gfold] ✅ gfold has been uninstalled from your system" + + if [ -f $HOME/.config/gfold/gfold.json ]; then + echo "[uninstall-gfold] ⚠️ you may want to delete or backup the config file" + echo "[uninstall-gfold] ⚠️ config file path: $HOME/.config/gfold/gfold.json" fi } -delete-binary /tmp/gfold -delete-binary /usr/local/bin/gfold -echo "gfold has been removed from your system"
\ No newline at end of file +perform-uninstall
\ No newline at end of file @@ -23,6 +23,10 @@ struct Args { description = "path to target directory (defaults to current working directory)" )] path: Option<String>, + + #[argh(switch, short = 'i', description = "ignore config file settings")] + ignore_config_file: bool, + #[argh(switch, description = "display results with classic formatting")] classic: bool, #[argh( @@ -37,9 +41,11 @@ struct Args { } pub fn parse() -> Result<()> { - // First and foremost, get logging up and running. + // 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); + match args.version { true => { println!("gfold {}", env!("CARGO_PKG_VERSION")); @@ -50,7 +56,10 @@ pub fn parse() -> Result<()> { } fn merge_config_and_run(args: &Args) -> Result<()> { - let mut config = Config::try_config()?; + let mut config = match args.ignore_config_file { + true => Config::new()?, + false => Config::try_config()?, + }; if let Some(s) = &args.path { config.path = env::current_dir()?.join(s).canonicalize()?; diff --git a/src/config.rs b/src/config.rs index 084d7a3..c15cdcf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,18 +7,35 @@ use std::fs::File; use std::io::BufReader; use std::path::PathBuf; -#[derive(Deserialize, Default)] -pub struct EntryConfig { - pub path: Option<PathBuf>, - pub display_mode: Option<DisplayMode>, -} - +// "Config" is the actual type consumed through the codebase. It is boostrapped via its public +// methods and uses "EntryConfig", a private struct, under the hood in order to deserialize empty, +// non-existent, partial, and complete config files. #[derive(Serialize)] pub struct Config { pub path: PathBuf, pub display_mode: DisplayMode, } +// "EntryConfig" is a reflection of "Config" with its fields wrapped as "Option" types. This is to +// ensure that we can deserialize from partial config file contents and populate empty fields with +// defaults. Moreover, enumerations cannot set defaults values currently, so we need to set +// desired defaults for the user. In this case, the public methods for "Config" use "EntryConfig" +// privately. +#[derive(Deserialize, Default)] +struct EntryConfig { + pub path: Option<PathBuf>, + pub display_mode: Option<DisplayMode>, +} + +// "DisplayMode" dictates which way the results gathered should be displayed to the user via +// STDOUT. Setting this enumeration is _mostly_ cosmetic, but it is possible that collected data +// may differ in order to reduce compute load. For example: if one display mode dislays more +// information than another display mode, more subcommands and functions might get executed. +// Conversely, if another display mode requires less information to be displayed, then some +// commands and functions migth get skipped. +// +// TLDR: while this setting is primarily for cosmetics, it may also affect runtime performance +// based on what needs to be displayed. #[derive(Serialize, Deserialize, Clone)] pub enum DisplayMode { Standard, @@ -26,6 +43,9 @@ pub enum DisplayMode { } 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 + // creating a config. pub fn try_config() -> Result<Config> { let home = dirs::home_dir().ok_or(Error::HomeDirNotFound)?; let entry_config = match File::open(home.join(".config").join("gfold").join("gfold.json")) { @@ -41,13 +61,21 @@ impl Config { entry_config_to_config(&entry_config) } + // This method does not look for the config file and uses "EntryConfig"'s defaults instead. + // This method is best for testing use and when the user wishes to skip config file lookup. + pub fn new() -> Result<Config> { + entry_config_to_config(&EntryConfig::default()) + } + + // This method prints the full config (merged with config file, as needed) as valid JSON. pub fn print(self) -> Result<()> { println!("{}", serde_json::to_string_pretty(&self)?); Ok(()) } } -pub fn entry_config_to_config(entry_config: &EntryConfig) -> Result<Config> { +// Internal conversion function for private "EntryConfig" objects to "Config" objects. +fn entry_config_to_config(entry_config: &EntryConfig) -> Result<Config> { Ok(Config { path: match &entry_config.path { Some(s) => s.clone(), diff --git a/src/main.rs b/src/main.rs index 5b73d43..56f42db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,20 +18,20 @@ fn main() -> Result<()> { #[cfg(test)] mod tests { use super::*; - use crate::config::EntryConfig; + use crate::config::Config; use crate::error::Error; use std::env; #[test] fn current_directory() { - let config = config::entry_config_to_config(&EntryConfig::default()).unwrap(); + let config = Config::new().unwrap(); assert!(run::run(&config).is_ok()); } #[test] fn parent_directory() { - let mut config = config::entry_config_to_config(&EntryConfig::default()).unwrap(); + let mut config = Config::new().unwrap(); let mut parent = env::current_dir().expect("failed to get current working directory"); parent.pop(); @@ -42,7 +42,7 @@ mod tests { #[test] fn home_directory() { - let mut config = config::entry_config_to_config(&EntryConfig::default()).unwrap(); + let mut config = Config::new().unwrap(); config.path = dirs::home_dir().ok_or(Error::HomeDirNotFound).unwrap(); diff --git a/src/report.rs b/src/report.rs index 79220c8..1a5151c 100644 --- a/src/report.rs +++ b/src/report.rs @@ -100,7 +100,7 @@ fn generate_report(path: &Path, include_email: bool) -> Result<Report> { status, status_as_string, branch: branch.to_string(), - url: match git(&["config", "remote.origin.url"], path)?.strip_suffix(NEWLINE) { + url: match git(&["config", "--get", "remote.origin.url"], path)?.strip_suffix(NEWLINE) { Some(s) => s.to_string(), None => NONE.to_string(), }, @@ -128,18 +128,15 @@ fn is_unpushed(path: &Path, branch: &str) -> Result<bool> { } fn get_email(path: &Path) -> Result<String> { - match git(&["config", "--get", "user.email"], path)?.strip_suffix(NEWLINE) { - Some(s) => Ok(s.to_string()), - None => Ok(NONE.to_string()), - } + Ok( + match git(&["config", "--get", "user.email"], path)?.strip_suffix(NEWLINE) { + Some(s) => s.to_string(), + None => NONE.to_string(), + }, + ) } fn git(args: &[&str], wd: &Path) -> Result<String> { - match Command::new("git").args(args).current_dir(wd).output() { - Ok(o) => match String::from_utf8(o.stdout) { - Ok(s) => Ok(s), - Err(e) => Err(e.into()), - }, - Err(e) => Err(e.into()), - } + let output = Command::new("git").args(args).current_dir(wd).output()?; + Ok(String::from_utf8(output.stdout)?) } @@ -5,6 +5,9 @@ use crate::report::Reports; use crate::target_gen::Targets; 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 +// set, merged, ignored, etc. pub fn run(config: &Config) -> Result<()> { let reports = Reports::new(Targets::new(&config.path)?, &config.display_mode)?; |