diff options
author | Nick Gerace <nickagerace@gmail.com> | 2022-12-20 14:15:41 -0500 |
---|---|---|
committer | Nick Gerace <nickagerace@gmail.com> | 2022-12-20 14:27:32 -0500 |
commit | 0dc427908638d1494a47d60e678d4b934c6355a9 (patch) | |
tree | 3f6b02d98fa4359cd9fd578784146bc060acc232 | |
parent | fa71d230e8f6efe28a1c9c11874432ca575e3440 (diff) | |
download | gfold-0dc427908638d1494a47d60e678d4b934c6355a9.zip |
Assume unpushed if unable to resolve ref from short name
- When checking if unpushed and attempting to resolve the reference from
a short name, ignore the error and assume we need to push
- For now, the error is logged to stderr at the debug level, but we
may need to perform more advanced error handling in the future
- Use tempfile instead of creating directories under "target"
- The root temporary directory and its descendants will be immediately
deleted as soon as the value for root is dropped from scope
- This is far less error prone than the previous method
- Remove bors to increase developer and release feedback loop
Signed-off-by: Nick Gerace <nickagerace@gmail.com>
-rw-r--r-- | .github/workflows/ci.yml | 5 | ||||
-rw-r--r-- | CHANGELOG.md | 9 | ||||
-rw-r--r-- | Cargo.lock | 52 | ||||
-rw-r--r-- | README.md | 5 | ||||
-rw-r--r-- | bors.toml | 8 | ||||
-rw-r--r-- | crates/bench-loosely/Cargo.toml | 2 | ||||
-rw-r--r-- | crates/gfold/Cargo.toml | 1 | ||||
-rw-r--r-- | crates/gfold/src/cli.rs | 2 | ||||
-rw-r--r-- | crates/gfold/src/config.rs | 4 | ||||
-rw-r--r-- | crates/gfold/src/main.rs | 278 | ||||
-rw-r--r-- | crates/gfold/src/report.rs | 14 | ||||
-rw-r--r-- | crates/size/Cargo.toml | 2 |
12 files changed, 209 insertions, 173 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e61c967..cde6f1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,11 +2,10 @@ name: ci on: push: branches: - - staging - - trying + - "main" pull_request: branches: - - main + - "main" paths: - "**.rs" - "Cargo.*" diff --git a/CHANGELOG.md b/CHANGELOG.md index 52aedff..ba217e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,14 +8,19 @@ For new changes prior to version 4.0.0, please see [CHANGELOG_PRE_V4](./docs/CHA ## Unreleased -The latest version contains all changes. +<!-- The latest version contains all changes. --> + +### Changed + +- Bump dependencies +- When checking if unpushed and attempting to resolve the reference from a short name, ignore the error and assume we need to push ## 4.1.1 - 2022-12-19 ### Changed +- Bump dependencies - Ensure dependencies have their minor version fields locked -- Update dependencies ### Notes @@ -241,6 +241,15 @@ dependencies = [ ] [[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] name = "form_urlencoded" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -274,6 +283,7 @@ dependencies = [ "rayon", "serde", "serde_json", + "tempfile", "termcolor", "thiserror", "toml", @@ -339,6 +349,15 @@ dependencies = [ ] [[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] name = "io-lifetimes" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -350,9 +369,9 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330" +checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" dependencies = [ "hermit-abi 0.2.6", "io-lifetimes", @@ -446,11 +465,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi 0.1.19", + "hermit-abi 0.2.6", "libc", ] @@ -593,6 +612,15 @@ dependencies = [ ] [[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] name = "rustc-demangle" version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -680,6 +708,20 @@ dependencies = [ ] [[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] name = "termcolor" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3,8 +3,7 @@ [![latest release tag](https://img.shields.io/github/v/tag/nickgerace/gfold?sort=semver&logo=git&logoColor=white&label=version&style=flat-square&color=blue)](https://github.com/nickgerace/gfold/releases/latest) [![crates.io version](https://img.shields.io/crates/v/gfold?style=flat-square&logo=rust&color=orange)](https://crates.io/crates/gfold) [![license](https://img.shields.io/github/license/nickgerace/gfold?style=flat-square&logo=apache&color=silver)](./LICENSE) -[![build status](https://img.shields.io/github/workflow/status/nickgerace/gfold/merge/main?style=flat-square&logo=github&logoColor=white)](https://github.com/nickgerace/gfold/actions?query=workflow%3Amerge+branch%3Amain) -[![bors enabled](https://bors.tech/images/badge_small.svg)](https://app.bors.tech/repositories/42509) +[![build status](https://img.shields.io/github/actions/workflow/status/nickgerace/gfold/workflows/ci.yml?branch=main&style=flat-square&logo=github&logoColor=white)](https://img.shields.io/github/actions/workflow/status/nickgerace/gfold/workflows/ci.yml?branch=main&style=flat-square&logo=github&logoColor=white) `gfold` is a CLI-driven application that helps you keep track of multiple Git repositories. @@ -220,4 +219,4 @@ Name | Type | Description [Arch Linux community repository](https://archlinux.org/packages/community/x86_64/gfold/) | packaging | the `gfold` package _(note: before moving to the community repository, the [AUR](https://github.com/orhun/PKGBUILDs) was previously used for distribution)_ ["One Hundred Rust Binaries"](https://www.wezm.net/v2/posts/2020/100-rust-binaries/page2/) | article | featured `gfold` [nixpkgs](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/version-management/git-and-tools/gfold/default.nix) | packaging | the `gfold` package -[nvim-gfold.lua](https://github.com/AckslD/nvim-gfold.lua) | project | a `neovim` plugin for `gfold` *([announcement Reddit post](https://www.reddit.com/r/neovim/comments/t209wy/introducing_nvimgfoldlua/))*
\ No newline at end of file +[nvim-gfold.lua](https://github.com/AckslD/nvim-gfold.lua) | project | a `neovim` plugin for `gfold` *([announcement Reddit post](https://www.reddit.com/r/neovim/comments/t209wy/introducing_nvimgfoldlua/))* diff --git a/bors.toml b/bors.toml deleted file mode 100644 index 83f1433..0000000 --- a/bors.toml +++ /dev/null @@ -1,8 +0,0 @@ -status = [ - "Lint / Test / Build (ubuntu-latest)", - "Test / Build (windows-latest)", - "Test / Build (macos-latest)" -] -timeout_sec = 900 -delete_merged_branches = true -update_base_for_deletes = true diff --git a/crates/bench-loosely/Cargo.toml b/crates/bench-loosely/Cargo.toml index b533aa1..d090406 100644 --- a/crates/bench-loosely/Cargo.toml +++ b/crates/bench-loosely/Cargo.toml @@ -4,4 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] -dirs = "4" +dirs = "4.0" diff --git a/crates/gfold/Cargo.toml b/crates/gfold/Cargo.toml index 7802f81..934674e 100644 --- a/crates/gfold/Cargo.toml +++ b/crates/gfold/Cargo.toml @@ -31,3 +31,4 @@ env_logger = { version = "0.9", features = ["atty", "humantime"], default_featur [dev-dependencies] pretty_assertions = "1.3" +tempfile = "3.3" diff --git a/crates/gfold/src/cli.rs b/crates/gfold/src/cli.rs index 4ccf474..8408bfd 100644 --- a/crates/gfold/src/cli.rs +++ b/crates/gfold/src/cli.rs @@ -65,7 +65,7 @@ pub fn parse_and_run() -> anyhow::Result<()> { debug!("collected args"); let mut config = match cli.ignore_config_file { - true => Config::new()?, + true => Config::try_config_default()?, false => Config::try_config()?, }; debug!("loaded initial config"); diff --git a/crates/gfold/src/config.rs b/crates/gfold/src/config.rs index bb4ac57..7562cb4 100644 --- a/crates/gfold/src/config.rs +++ b/crates/gfold/src/config.rs @@ -86,8 +86,8 @@ impl Config { } /// 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() -> io::Result<Self> { + /// Use this method when the user wishes to skip config file lookup. + pub fn try_config_default() -> io::Result<Self> { Self::from_entry_config(&EntryConfig::default()) } diff --git a/crates/gfold/src/main.rs b/crates/gfold/src/main.rs index eec0790..6b7bdb6 100644 --- a/crates/gfold/src/main.rs +++ b/crates/gfold/src/main.rs @@ -37,142 +37,148 @@ mod tests { use crate::status::Status; use anyhow::{anyhow, Result}; + use git2::ErrorCode; + use git2::Oid; use git2::Repository; use git2::Signature; - use git2::Oid; + use pretty_assertions::assert_eq; use std::collections::BTreeMap; + use std::fs::File; use std::path::{Path, PathBuf}; - use std::{env, fs, io}; + use std::{fs, io}; + use tempfile::tempdir; - /// This integration test for `gfold` covers an end-to-end usage scenario. It does not - /// _remove_ anything in the filesystem (for saftey), so you must delete the `test` - /// directory underneath `target` to regenerate a clean dataset. + /// This integration test for `gfold` covers an end-to-end usage scenario. It uses the + /// [`tempfile`](tempfile) crate to create some repositories with varying states and levels + /// of nesting. #[test] fn integration() -> Result<()> { - // Test directory structure within "target": - // └── test - // ├── bar - // ├── baz - // ├── foo - // │ └── newfile + env_logger::builder() + .is_test(true) + .filter_level(LevelFilter::Info) + .try_init()?; + + // Temporary directory structure: + // └── root + // ├── one (repo) + // │ └── file + // ├── two (repo) + // ├── three (repo) // └── nested - // ├── one - // │ └── newfile - // ├── three - // └── two - let test_directory = integration_init()?; - create_directory(&test_directory)?; - - for name in ["foo", "bar", "baz"] { - let current = test_directory.join(name); - create_directory(¤t)?; - Repository::init(¤t)?; - - if name == "foo" { - create_file(¤t.join("newfile"))?; - } - } + // ├── four (repo) + // ├── five (repo) + // │ └── file + // ├── six (repo) + // └── seven (repo) + let root = tempdir()?; + let repo_one = create_directory(&root, "one")?; + let repo_two = create_directory(&root, "two")?; + let repo_three = create_directory(&root, "three")?; - let nested = test_directory.join("nested"); - create_directory(&nested)?; - for name in ["one", "two", "three"] { - let current = nested.join(name); - create_directory(¤t)?; - let repository = Repository::init(¤t)?; + let nested = create_directory(&root, "nested")?; + let repo_four = create_directory(&nested, "four")?; + let repo_five = create_directory(&nested, "five")?; + let repo_six = create_directory(&nested, "six")?; + let repo_seven = create_directory(&nested, "seven")?; - if name == "one" { - create_file(¤t.join("newfile"))?; - } + // Repo One + Repository::init(&repo_one)?; + create_file(&repo_one)?; + + // Repo Two + Repository::init(&repo_two)?; + + // Repo Three + Repository::init(&repo_three)?; - if name == "two" { - if let Err(e) = repository.remote("origin", "https://github.com/nickgerace/gfold") { - if e.code() != ErrorCode::Exists { - return Err(e.into()); - } - } + // Repo Four + let repository = Repository::init(&repo_four)?; + if let Err(e) = repository.remote("origin", "https://github.com/nickgerace/gfold") { + if e.code() != ErrorCode::Exists { + return Err(e.into()); } + } - if name == "three" { - if let Err(e) = repository.remote("fork", "https://github.com/nickgerace/gfold") { - if e.code() != ErrorCode::Exists { - return Err(e.into()); - } - } + // Repo Five + Repository::init(&repo_five)?; + create_file(&repo_five)?; - create_branch(&repository, "feat")?; + // Repo Six + let repository = Repository::init(&repo_six)?; + if let Err(e) = repository.remote("fork", "https://github.com/nickgerace/gfold") { + if e.code() != ErrorCode::Exists { + return Err(e.into()); } } + commit_head_and_create_branch(&repository, "feat")?; - let mut config = Config::new()?; - config.path = test_directory.clone(); + // Repo Seven + let repository = Repository::init(&repo_seven)?; + if let Err(e) = repository.remote("origin", "https://github.com/nickgerace/gfold") { + if e.code() != ErrorCode::Exists { + return Err(e.into()); + } + } + commit_head_and_create_branch(&repository, "needtopush")?; + repository.set_head("refs/heads/needtopush")?; + + // Run once with default display mode. + let mut config = Config::try_config_default()?; + config.path = root.path().to_path_buf(); config.color_mode = ColorMode::Never; - assert!(run::run(&config).is_ok()); + run::run(&config)?; // Now, let's ensure our reports are what we expect. let mut expected_reports: LabeledReports = BTreeMap::new(); - - let key = test_directory + let expected_reports_key = root + .path() .to_str() .ok_or_else(|| anyhow!("could not convert PathBuf to &str"))? .to_string(); - let mut reports = vec![ - Report::new( - &test_directory.join("foo"), - "HEAD", - &Status::Unclean, - None, - None, - )?, - Report::new( - &test_directory.join("bar"), - "HEAD", - &Status::Clean, - None, - None, - )?, - Report::new( - &test_directory.join("baz"), - "HEAD", - &Status::Clean, - None, - None, - )?, + let mut expected_reports_raw = vec![ + Report::new(&repo_one, "HEAD", &Status::Unclean, None, None)?, + Report::new(&repo_two, "HEAD", &Status::Clean, None, None)?, + Report::new(&repo_three, "HEAD", &Status::Clean, None, None)?, ]; - reports.sort_by(|a, b| a.name.cmp(&b.name)); - expected_reports.insert(Some(key), reports); + expected_reports_raw.sort_by(|a, b| a.name.cmp(&b.name)); + expected_reports.insert(Some(expected_reports_key), expected_reports_raw); - let nested_test_dir = test_directory.join("nested"); - let key = nested_test_dir + // Add nested reports to the expected reports map. + let nested_expected_reports_key = nested .to_str() .ok_or_else(|| anyhow!("could not convert PathBuf to &str"))? .to_string(); - let mut reports = vec![ + let mut nested_expected_reports_raw = vec![ Report::new( - &nested_test_dir.join("one"), + &repo_four, "HEAD", - &Status::Unclean, - None, + &Status::Clean, + Some("https://github.com/nickgerace/gfold".to_string()), None, )?, + Report::new(&repo_five, "HEAD", &Status::Unclean, None, None)?, Report::new( - &nested_test_dir.join("two"), - "HEAD", - &Status::Clean, + &repo_six, + "master", + &Status::Unpushed, Some("https://github.com/nickgerace/gfold".to_string()), None, )?, Report::new( - &nested_test_dir.join("three"), - "HEAD", - &Status::Clean, + &repo_seven, + "needtopush", + &Status::Unpushed, Some("https://github.com/nickgerace/gfold".to_string()), None, )?, ]; - reports.sort_by(|a, b| a.name.cmp(&b.name)); - expected_reports.insert(Some(key), reports); + nested_expected_reports_raw.sort_by(|a, b| a.name.cmp(&b.name)); + expected_reports.insert( + Some(nested_expected_reports_key), + nested_expected_reports_raw, + ); // Use classic display mode to avoid collecting email results. config.display_mode = DisplayMode::Classic; @@ -184,87 +190,75 @@ mod tests { found_labeled_reports_sorted.insert(labeled_report.0.clone(), value.clone()); } - assert_eq!(found_labeled_reports_sorted, expected_reports); + assert_eq!( + expected_reports, // expected + found_labeled_reports_sorted // actual + ); Ok(()) } - /// Ensure we are underneath the repository root. Safely create the test directory. - fn integration_init() -> Result<PathBuf> { - let manifest_directory = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let repo_root = manifest_directory - .parent() - .ok_or_else(|| anyhow!("could not get parent"))? - .parent() - .ok_or_else(|| anyhow!("could not get parent"))?; - assert!(Repository::open(repo_root).is_ok()); - - let target = repo_root.join("target"); - create_directory(&target)?; - let test = target.join("test"); - Ok(test) - } + fn create_directory<P: AsRef<Path>>(parent: P, name: &str) -> Result<PathBuf> { + let parent = parent.as_ref(); + let new_directory = parent.join(name); - fn create_directory(path: &Path) -> Result<()> { - if let Err(e) = fs::create_dir(path) { + if let Err(e) = fs::create_dir(&new_directory) { if e.kind() != io::ErrorKind::AlreadyExists { return Err(anyhow!( "could not create directory ({:?}) due to error kind: {:?}", - path, + &new_directory, e.kind() )); } } - Ok(()) + Ok(new_directory) } - fn create_file(path: &Path) -> Result<()> { - if let Err(e) = fs::File::create(path) { - if e.kind() != io::ErrorKind::AlreadyExists { - return Err(anyhow!( - "could not create file ({:?}) due to error kind: {:?}", - path, - e.kind() - )); - } - } + fn create_file<P: AsRef<Path>>(parent: P) -> Result<()> { + let parent = parent.as_ref(); + File::create(parent.join("file"))?; Ok(()) } - fn create_branch(repository: &Repository, name: &str) -> Result<()> { - // we need to commit something before branching - let commit_oid = commit(repository)?; - repository.branch(name, &repository.find_commit(commit_oid).unwrap(), true).unwrap(); - + fn commit_head_and_create_branch(repository: &Repository, name: &str) -> Result<()> { + // We need to commit at least once before branching. + let commit_oid = commit(repository, "HEAD")?; + let commit = repository.find_commit(commit_oid)?; + repository.branch(name, &commit, true)?; Ok(()) } - // taken from https://github.com/rust-lang/git2-rs/pull/885 - fn commit(repository: &Repository) -> Result<Oid> { - // We will commit the content of the index + // Source: https://github.com/rust-lang/git2-rs/pull/885 + fn commit(repository: &Repository, update_ref: &str) -> Result<Oid> { + // We will commit the contents of the index. let mut index = repository.index()?; let tree_oid = index.write_tree()?; let tree = repository.find_tree(tree_oid)?; - let parent_commit = match repository.revparse_single("HEAD") { - Ok(obj) => Some(obj.into_commit().unwrap()), - // First commit so no parent commit + // If this is the first commit, there is no parent. If the object returned by + // "revparse_single" cannot be converted into a commit, then it isn't a commit and we know + // there is no parent _commit_. + let maybe_parent = match repository.revparse_single("HEAD") { + Ok(object) => match object.into_commit() { + Ok(commit) => Some(commit), + Err(_) => None, + }, Err(e) if e.code() == ErrorCode::NotFound => None, - Err(_e) => panic!(), + Err(e) => return Err(e.into()), }; let mut parents = Vec::new(); - if parent_commit.is_some() { - parents.push(parent_commit.as_ref().unwrap()); - } + if let Some(parent) = maybe_parent.as_ref() { + parents.push(parent); + }; - let sig = Signature::now("Bob", "bob@bob").unwrap(); + let signature = Signature::now("Bob", "bob@bob")?; Ok(repository.commit( - Some("HEAD"), - &sig, - &sig, + Some(update_ref), + &signature, + &signature, "hello", &tree, - &parents[..], + parents.as_ref(), )?) } } diff --git a/crates/gfold/src/report.rs b/crates/gfold/src/report.rs index 4d5c20c..6043235 100644 --- a/crates/gfold/src/report.rs +++ b/crates/gfold/src/report.rs @@ -103,7 +103,7 @@ pub fn generate_reports(path: &Path, display_mode: &DisplayMode) -> anyhow::Resu /// Generates a report with a given path. fn generate_report(repo_path: &Path, include_email: bool) -> anyhow::Result<Report> { debug!( - "attemping to generate report for repository at path: {:?}", + "attempting to generate report for repository at path: {:?}", repo_path ); let repo = Repository::open(repo_path)?; @@ -179,14 +179,18 @@ fn is_unpushed( match head.shorthand() { Some(v) => v, None => { - trace!("assuming unpushed; could not determine shorthand for head"); + debug!("assuming unpushed; could not determine shorthand for head"); return Ok(true); } } ); - let remote_head = repo - .resolve_reference_from_short_name(&remote)? - .peel_to_commit()?; + let remote_head = match repo.resolve_reference_from_short_name(&remote) { + Ok(reference) => reference.peel_to_commit()?, + Err(e) => { + debug!("assuming unpushed; could not resolve remote reference from short name (ignored error: {})", e); + return Ok(true); + } + }; Ok( matches!(repo.graph_ahead_behind(local_head.id(), remote_head.id()), Ok(number_unique_commits) if number_unique_commits.0 > 0), ) diff --git a/crates/size/Cargo.toml b/crates/size/Cargo.toml index 1a6bc06..9b56e93 100644 --- a/crates/size/Cargo.toml +++ b/crates/size/Cargo.toml @@ -4,4 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] -dirs = "4" +dirs = "4.0" |