From cf1ecd64e758893a485f77b78ed3c4fb64595d8d Mon Sep 17 00:00:00 2001 From: adamperkowski Date: Fri, 22 Nov 2024 01:21:14 +0000 Subject: [PATCH 01/14] changelog for 90d50ab --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ca55c5..b8be0ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to nvrs will be documented in this file. +## [upstream] + +### πŸ› Bug Fixes + +- (*verfile*) allow missing gitref & url ([b93216d](https://github.com/adamperkowski/nvrs/commit/b93216d5146a672897e11938668e05cfa859cfac)) + +### πŸ“š Documentation + +- (*git-cliff*) add `UI/UX` ([42727ad](https://github.com/adamperkowski/nvrs/commit/42727ad6bd020ecee06e93017e7e5b68851c01d3)) +- (*config*) fix the package name (alpm -> mkinitcpio) ([1327516](https://github.com/adamperkowski/nvrs/commit/132751692941f5e1e2cce188d545f3ee421dad46)) +- better banner ([a4718b6](https://github.com/adamperkowski/nvrs/commit/a4718b60505d26c2e262b70d77160b475b8f2348)) +- (*dependabot*) change cargo commit message ([90d50ab](https://github.com/adamperkowski/nvrs/commit/90d50ab0fd6cd4964408796e2f75affeb539923b)) + +### 🧩 UI/UX + +- (*output*) print out `NONE` take information ([71cb36f](https://github.com/adamperkowski/nvrs/commit/71cb36f913035d484bf26d8a2c3430132ea176ba)) + ## [0.1.3] - 2024-11-18 ### πŸ› Bug Fixes From f2e22b6c8daece310080a8e32d183e0f6ef3e3f0 Mon Sep 17 00:00:00 2001 From: Adam Perkowski Date: Sat, 23 Nov 2024 02:48:18 +0100 Subject: [PATCH 02/14] =?UTF-8?q?docs:=20=F0=9F=9A=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adam Perkowski --- Cargo.toml | 2 +- README.md | 2 +- man/nvrs.1 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6f61531..16abf05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "nvrs" version = "0.1.3" authors = ["Adam Perkowski "] license = "MIT" -description = "new version checker for software releases πŸ¦€" +description = "🚦 fast new version checker for software releases πŸ¦€" repository = "https://github.com/adamperkowski/nvrs" readme = "README.md" categories = ["command-line-interface", "command-line-utilities"] diff --git a/README.md b/README.md index 6ec192d..626886e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
# nvrs -new version checker for software releases πŸ¦€
+🚦 fast new version checker for software releases πŸ¦€
[nvchecker](https://github.com/lilydjwg/nvchecker) rewritten in Rust ![GitHub Contributors](https://img.shields.io/github/contributors-anon/adamperkowski/nvrs?style=for-the-badge&labelColor=%23a8127d&color=%23336795) ![GitHub Repo Size](https://img.shields.io/github/repo-size/adamperkowski/nvrs?style=for-the-badge&labelColor=%23a8127d&color=%23336795) ![Repo Created At](https://img.shields.io/github/created-at/adamperkowski/nvrs?style=for-the-badge&labelColor=%23a8127d&color=%23336795) diff --git a/man/nvrs.1 b/man/nvrs.1 index 64a4762..ca90deb 100644 --- a/man/nvrs.1 +++ b/man/nvrs.1 @@ -2,7 +2,7 @@ .TH "nvrs" "1" "November 2024" "" "nvrs manual" .SH NAME -nvrs \- fast new version checker for software releases πŸ¦€ +nvrs \- fast new version checker for software releases πŸš¦πŸ¦€ .SH SYNOPSIS \fBnvrs [OPTIONS]\fR From c0021f0a4e02791802fba9ba6bca5486f825ee4e Mon Sep 17 00:00:00 2001 From: Adam Perkowski Date: Sun, 24 Nov 2024 21:17:52 +0100 Subject: [PATCH 03/14] refact(codebase)!: move internal logic to `lib` (#4) This refactors **the whole codebase** and makes some additions, for example: - better documentation - performance improvements - error handling - integration & unit tests - other stuff i forgot about --------- Signed-off-by: Adam Perkowski --- .github/workflows/rust.yml | 20 +- .gitignore | 2 + CONTRIBUTING.md | 2 + Cargo.lock | 71 ++++++- Cargo.toml | 19 +- README.md | 3 +- n_keyfile.toml | 6 + nvrs.toml | 3 + src/api/aur.rs | 82 ++++---- src/api/github.rs | 75 ++++---- src/api/gitlab.rs | 108 +++++------ src/api/mod.rs | 67 +++++-- src/cli.rs | 74 ++++++++ src/config.rs | 184 ++++++++---------- src/error.rs | 83 ++++++++ src/keyfile.rs | 70 +++++++ src/lib.rs | 83 ++++++++ src/main.rs | 375 +++++++++++++++++-------------------- src/verfiles.rs | 85 ++++----- 19 files changed, 900 insertions(+), 512 deletions(-) create mode 100644 src/cli.rs create mode 100644 src/error.rs create mode 100644 src/keyfile.rs create mode 100644 src/lib.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 72956c0..6b82f09 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -4,17 +4,20 @@ on: branches: ["main"] paths: - '**/*.rs' - - 'src/**' + - 'src/**/*' - 'Cargo.toml' - 'Cargo.lock' pull_request: paths: - '**/*.rs' - - 'src/**' + - 'src/**/*' - 'Cargo.toml' - 'Cargo.lock' workflow_dispatch: +env: + CARGO_TERM_COLOR: always + jobs: check: runs-on: ubuntu-latest @@ -45,3 +48,16 @@ jobs: cargo build --verbose cargo build --release --verbose + test: + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v4 + + - name: Test + run: | + cargo test --lib + cargo test --lib --release + cargo test --doc + cargo test --doc --release diff --git a/.gitignore b/.gitignore index 0431dd9..d40e8fa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ newver.json oldver.json keyfile.toml +*_old +*.old diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1b6e3ce..1822d94 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,8 @@ Thank you for considering contributing to [nvrs](https://github.com/adamperkowski/nvrs) ❀️ +If, while viewing the code, you find any parts unclear or unexplained, please [open an issue](https://github.com/adamperkowski/nvrs/issues/new/choose) with a documentation request. + Note that we have a [Code of Conduct](./CODE_OF_CONDUCT.md). Please follow it in all your interactions with the project. ## Workflow diff --git a/Cargo.lock b/Cargo.lock index 403db5b..afa9340 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,6 +66,28 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -798,16 +820,17 @@ dependencies = [ [[package]] name = "nvrs" -version = "0.1.3" +version = "0.1.4-pre1" dependencies = [ "clap", "colored", "futures", - "lazy_static", "reqwest", "serde", "serde_json", + "thiserror", "tokio", + "tokio-test", "toml", ] @@ -1258,6 +1281,26 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "thiserror" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -1318,6 +1361,30 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + [[package]] name = "tokio-util" version = "0.7.12" diff --git a/Cargo.toml b/Cargo.toml index 16abf05..0dc1577 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nvrs" -version = "0.1.3" +version = "0.1.4-pre1" authors = ["Adam Perkowski "] license = "MIT" description = "🚦 fast new version checker for software releases πŸ¦€" @@ -10,6 +10,7 @@ categories = ["command-line-interface", "command-line-utilities"] edition = "2021" include = [ "**/*.rs", + "src/**/*", "Cargo.toml", "README.md", "LICENSE", @@ -19,22 +20,26 @@ include = [ ] [features] -default = ["aur", "github", "gitlab"] -aur = ["reqwest"] -github = ["reqwest"] -gitlab = ["reqwest"] +default = ["http", "aur", "github", "gitlab"] +http = ["reqwest"] +aur = ["http"] +github = ["http"] +gitlab = ["http"] [dependencies] clap = { version = "4.5.21", features = ["derive", "color", "error-context", "help", "std", "usage"], default-features = false } colored = "2.1.0" futures = "0.3.31" -lazy_static = "1.5.0" reqwest = { version = "0.12.9", features = ["__tls", "charset", "default-tls", "h2", "http2", "json"], default-features = false, optional = true } -serde = { version = "1.0.215", features = ["derive"] } +serde = { version = "1.0.215", features = ["derive"], default-features = false } serde_json = "1.0.132" +thiserror = "2.0.3" tokio = { version = "1.41.1", features = ["full"] } toml = { version = "0.8.19", features = ["parse", "display"], default-features = false } +[dev-dependencies] +tokio-test = "0.4.4" + [profile.release] lto = "fat" codegen-units = 1 diff --git a/README.md b/README.md index 626886e..a4fcb09 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ 🚦 fast new version checker for software releases πŸ¦€
[nvchecker](https://github.com/lilydjwg/nvchecker) rewritten in Rust +![Build Status](https://img.shields.io/github/actions/workflow/status/adamperkowski/nvrs/rust.yml?style=for-the-badge&labelColor=%23a8127d&color=%23336795) ![docs.rs](https://img.shields.io/docsrs/nvrs?style=for-the-badge&labelColor=%23a8127d&color=%23336795)
![GitHub Contributors](https://img.shields.io/github/contributors-anon/adamperkowski/nvrs?style=for-the-badge&labelColor=%23a8127d&color=%23336795) ![GitHub Repo Size](https://img.shields.io/github/repo-size/adamperkowski/nvrs?style=for-the-badge&labelColor=%23a8127d&color=%23336795) ![Repo Created At](https://img.shields.io/github/created-at/adamperkowski/nvrs?style=for-the-badge&labelColor=%23a8127d&color=%23336795) ![banner](/banner.webp) @@ -22,7 +23,7 @@ you may encounter some issues. please consider [submitting feedback](https://git | command | time per **updated** package | details | |---------------|------------------------------|--------------------------------------------------------| -| `nvrs` | ~ 0.04s | **API requests included**
depends on internet speed | +| `nvrs` | ~ 0.03s | **API requests included**
depends on internet speed | | `nvrs --cmp` | ~ 0.0008s | depends on disk speed | | `nvrs --take` | ~ 0.001s | depends on disk speed | diff --git a/n_keyfile.toml b/n_keyfile.toml index 3209e3f..247f4be 100644 --- a/n_keyfile.toml +++ b/n_keyfile.toml @@ -1,3 +1,9 @@ +# nvrs +# https://github.com/adamperkowski/nvrs + +# this is an example key configuration file +# see `nvrs.toml` + [keys] # github = "[REDACTED]" # gitlab = "[REDACTED]" diff --git a/nvrs.toml b/nvrs.toml index e37013c..0355018 100644 --- a/nvrs.toml +++ b/nvrs.toml @@ -1,6 +1,9 @@ # nvrs # https://github.com/adamperkowski/nvrs +# this is an example configuration file for nvrs +# use `use_latest_release = true` for maximum nvchecker compatibility + [__config__] oldver = "oldver.json" newver = "newver.json" diff --git a/src/api/aur.rs b/src/api/aur.rs index 09d2d56..b9eab93 100644 --- a/src/api/aur.rs +++ b/src/api/aur.rs @@ -1,43 +1,49 @@ -use reqwest::{ - header::{HeaderMap, HeaderValue, USER_AGENT}, - StatusCode, -}; +use crate::{api, error}; -pub fn get_latest(package: String, _: Vec, _: String) -> crate::api::ReleaseFuture { +#[derive(serde::Deserialize)] +struct AURResponse { + results: Vec, +} + +#[allow(non_snake_case)] +#[derive(serde::Deserialize)] +struct AURResult { + Version: String, +} + +pub fn get_latest(args: api::ApiArgs) -> api::ReleaseFuture { Box::pin(async move { - let url = format!("https://aur.archlinux.org/rpc/v5/info/{}", package); - let mut headers = HeaderMap::new(); - headers.insert(USER_AGENT, HeaderValue::from_static("nvrs")); - let client = reqwest::Client::new(); - - let result = client.get(url).headers(headers).send().await.unwrap(); - - match result.status() { - StatusCode::OK => (), - status => { - crate::custom_error("GET request didn't return 200", format!("\n{}", status), ""); - return None; - } - } + let url = format!("https://aur.archlinux.org/rpc/v5/info/{}", args.args[0]); + let client = args.request_client; + + let result = client.get(url).headers(api::setup_headers()).send().await?; + api::match_statuscode(&result.status(), args.package.clone())?; - let json: serde_json::Value = result.json().await.unwrap(); - let first_result = json.get("results").unwrap().get(0).unwrap(); - - Some(crate::api::Release { - tag_name: first_result - .get("Version") - .unwrap() - .to_string() - .split('-') - .next() - .unwrap_or("") - .replace("\"", "") - .to_string(), - html_url: first_result - .get("URL") - .unwrap() - .to_string() - .replace("\"", ""), - }) + let json: AURResponse = result.json().await?; + + if let Some(first) = json.results.first() { + let version = first.Version.split_once('-').unwrap(); + + Ok(api::Release { + name: version.0.to_string(), + tag: None, + url: String::new(), + }) + } else { + Err(error::Error::NoVersion(args.package)) + } }) } + +#[tokio::test] +async fn request_test() { + let package = "permitter".to_string(); + let args = api::ApiArgs { + package: package.clone(), + args: vec![package], + api_key: String::new(), + request_client: reqwest::Client::new(), + }; + + assert!(get_latest(args).await.is_ok()); +} diff --git a/src/api/github.rs b/src/api/github.rs index 76398c4..5c87000 100644 --- a/src/api/github.rs +++ b/src/api/github.rs @@ -1,49 +1,56 @@ -use reqwest::{ - header::{HeaderMap, HeaderValue, ACCEPT, AUTHORIZATION, USER_AGENT}, - StatusCode, -}; +use reqwest::header::{HeaderValue, ACCEPT, AUTHORIZATION}; -pub fn get_latest(package: String, repo: Vec, key: String) -> crate::api::ReleaseFuture { +use crate::api; + +#[derive(serde::Deserialize)] +struct GitHubResponse { + tag_name: String, + html_url: String, +} + +pub fn get_latest(args: api::ApiArgs) -> api::ReleaseFuture { Box::pin(async move { - let url = format!("https://api.github.com/repos/{}/releases/latest", repo[0]); - let mut headers = HeaderMap::new(); + let url = format!( + "https://api.github.com/repos/{}/releases/latest", + args.args[0] + ); + let mut headers = api::setup_headers(); headers.insert( ACCEPT, HeaderValue::from_static("application/vnd.github+json"), ); - headers.insert(USER_AGENT, HeaderValue::from_static("nvrs")); headers.insert( "X-GitHub-Api-Version", HeaderValue::from_static("2022-11-28"), ); - if !key.is_empty() { - let bearer = format!("Bearer {}", key); + if !args.api_key.is_empty() { + let bearer = format!("Bearer {}", args.api_key); headers.insert(AUTHORIZATION, HeaderValue::from_str(&bearer).unwrap()); } - let client = reqwest::Client::new(); - - let result = client.get(url).headers(headers).send().await.unwrap(); - - match result.status() { - StatusCode::OK => (), - StatusCode::FORBIDDEN => { - crate::custom_error( - "GET request returned 430: ", - format!("{}\nwe might be getting rate-limited here", package), - "", - ); - return None; - } - status => { - crate::custom_error( - "GET request didn't return 200: ", - format!("{}\n{}", package, status), - "", - ); - return None; - } - } + let client = args.request_client; - Some(result.json().await.unwrap()) + let result = client.get(url).headers(headers).send().await?; + api::match_statuscode(&result.status(), args.package)?; + + let json: GitHubResponse = result.json().await?; + + Ok(api::Release { + name: json.tag_name.clone(), + tag: Some(json.tag_name), + url: json.html_url, + }) }) } + +#[tokio::test] +async fn request_test() { + let package = "nvrs".to_string(); + let args = api::ApiArgs { + package: package.clone(), + args: vec![format!("adamperkowski/{}", package)], + api_key: String::new(), + request_client: reqwest::Client::new(), + }; + + assert!(get_latest(args).await.is_ok()); +} diff --git a/src/api/gitlab.rs b/src/api/gitlab.rs index f551492..1d1b34f 100644 --- a/src/api/gitlab.rs +++ b/src/api/gitlab.rs @@ -1,74 +1,58 @@ -use reqwest::{ - header::{HeaderMap, HeaderName, HeaderValue, USER_AGENT}, - StatusCode, -}; -use serde_json::Value; +use crate::api; +use reqwest::header::HeaderValue; -pub fn get_latest(package: String, args: Vec, key: String) -> crate::api::ReleaseFuture { +#[derive(serde::Deserialize)] +struct GitLabResponse { + tag_name: String, + tag_path: String, +} + +pub fn get_latest(args: api::ApiArgs) -> api::ReleaseFuture { Box::pin(async move { - let url = if !args[1].is_empty() { - format!( - "https://{}/api/v4/projects/{}/releases/permalink/latest", - args[1], - args[0].replace("/", "%2F") - ) + let host = if !args.args[1].is_empty() { + &args.args[1] } else { - format!( - "https://gitlab.com/api/v4/projects/{}/releases/permalink/latest", - args[0].replace("/", "%2F") - ) + "gitlab.com" + }; + let url = format!( + "https://{}/api/v4/projects/{}/releases/permalink/latest", + host, + args.args[0].replace("/", "%2F") + ); + let mut headers = api::setup_headers(); + if !args.api_key.is_empty() { + headers.insert( + "PRIVATE-TOKEN", + HeaderValue::from_str(&args.api_key).unwrap(), + ); }; + let client = args.request_client; - let result = request(url, package, key).await.unwrap(); - let r_json: Value = result.json().await.unwrap(); + let result = client.get(url).headers(headers).send().await?; + api::match_statuscode(&result.status(), args.package)?; - Some(crate::api::Release { - tag_name: r_json - .get("tag_name") - .unwrap() - .to_string() - .replace("\"", ""), - html_url: r_json - .get("_links") - .unwrap() - .get("self") - .unwrap() - .to_string() - .replace("\"", ""), + let json: GitLabResponse = result.json().await?; + + Ok(api::Release { + name: json.tag_name.clone(), + tag: Some(json.tag_name), + url: format!("https://{}{}", host, json.tag_path), }) }) } -async fn request(url: String, package: String, key: String) -> Option { - let mut headers = HeaderMap::new(); - headers.insert(USER_AGENT, HeaderValue::from_static("nvrs")); - if !key.is_empty() { - headers.insert( - HeaderName::from_static("PRIVATE-TOKEN"), - HeaderValue::from_str(key.as_str()).unwrap(), - ); - } - let client = reqwest::Client::new(); - - let result = client.get(url).headers(headers).send().await.unwrap(); +#[tokio::test] +async fn request_test() { + let package = "mkinitcpio".to_string(); + let args = api::ApiArgs { + package: package.clone(), + args: vec![ + format!("archlinux/{0}/{0}", package), + "gitlab.archlinux.org".to_string(), + ], + api_key: String::new(), + request_client: reqwest::Client::new(), + }; - match result.status() { - StatusCode::OK => Some(result), - StatusCode::FORBIDDEN => { - crate::custom_error( - "GET request returned 430: ", - format!("{}\nwe might be getting rate-limited here", package), - "", - ); - None - } - status => { - crate::custom_error( - "GET request didn't return 200: ", - format!("{}\n{}", package, status), - "", - ); - None - } - } + assert!(get_latest(args).await.is_ok()); } diff --git a/src/api/mod.rs b/src/api/mod.rs index 250b3be..cbdeb92 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,26 +1,61 @@ -use serde::Deserialize; +//! this module handles management & communication with sources, also knows as APIs #[cfg(feature = "aur")] -pub mod aur; +mod aur; #[cfg(feature = "github")] -pub mod github; +mod github; #[cfg(feature = "gitlab")] -pub mod gitlab; +mod gitlab; -#[derive(Deserialize)] +/// struct containing the API name & a pointer to API's `get_latest` function +pub struct Api { + pub name: &'static str, + pub func: fn(ApiArgs) -> ReleaseFuture, +} + +/// arguments passed to a source +pub struct ApiArgs { + pub request_client: reqwest::Client, + pub package: String, + pub args: Vec, + pub api_key: String, // empty String if none +} + +/// this is what `get_latest`s return +#[derive(Debug)] pub struct Release { - pub tag_name: String, - pub html_url: String, + pub name: String, + pub tag: Option, + pub url: String, } -pub type ReleaseFuture = - std::pin::Pin> + Send>>; +// this is necessary because we need to store a reference to an async function in `Api` +type ReleaseFuture = + std::pin::Pin> + Send>>; -pub struct Api { - pub name: &'static str, - pub func: fn(String, Vec, String) -> ReleaseFuture, +#[cfg(feature = "http")] +fn setup_headers() -> reqwest::header::HeaderMap { + use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT}; + + let mut headers = HeaderMap::new(); + headers.insert(USER_AGENT, HeaderValue::from_static("nvrs")); + + headers +} + +#[cfg(feature = "http")] +fn match_statuscode(status: &reqwest::StatusCode, package: String) -> crate::error::Result<()> { + use crate::error; + use reqwest::StatusCode; + + match status.to_owned() { + StatusCode::OK => Ok(()), + StatusCode::FORBIDDEN => Err(error::Error::RequestForbidden(package)), + _ => Err(error::Error::RequestNotOK(package, status.to_string())), + } } +/// public list of available sources pub const API_LIST: &[Api] = &[ #[cfg(feature = "aur")] Api { @@ -38,3 +73,11 @@ pub const API_LIST: &[Api] = &[ func: gitlab::get_latest, }, ]; + +#[test] +fn statuscode_matching_test() { + use reqwest::StatusCode; + + assert!(match_statuscode(&StatusCode::OK, String::new()).is_ok()); + assert!(match_statuscode(&StatusCode::IM_A_TEAPOT, String::new()).is_err()); +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..ed28248 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,74 @@ +use clap::Parser; +use std::time::{SystemTime, UNIX_EPOCH}; + +const COPYRIGHT_TEXT: &str = + "Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the \"Software\"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:\n +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software."; + +#[derive(Clone, Parser)] +#[command(version, about)] +pub struct Cli { + #[arg( + short = 'c', + long, + help = "Compare newver with oldver and display differences as updates" + )] + pub cmp: bool, + + #[arg( + short = 't', + long, + value_name = "packages", + help = "List of packages to update automatically, separated by a comma", + value_delimiter = ',' + )] + pub take: Option>, + + #[arg( + short = 'n', + long, + value_name = "packages", + help = "List of packages to delete from the config", + value_delimiter = ',' + )] + pub nuke: Option>, + + #[arg( + long = "config", + value_name = "path", + help = "Override path to the config file" + )] + pub custom_config: Option, + + #[arg(long, help = "Don't exit on recoverable errors")] + pub no_fail: bool, + + #[arg(long, help = "Display copyright information")] + copyright: bool, +} + +pub fn get_args() -> Cli { + let cli = Cli::parse(); + + if cli.copyright { + let current_year = 1970 + + (SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time went backwards") + .as_secs() + / (365 * 24 * 60 * 60)); + + println!( + "Copyright (c) {} Adam Perkowski\n{}", + current_year, COPYRIGHT_TEXT + ); + } + + cli +} diff --git a/src/config.rs b/src/config.rs index 53f3a7f..b4e1d05 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,110 +1,102 @@ +//! operations on configuration files +//! +//! see the [example `nvrs.toml`](https://github.com/adamperkowski/nvrs/blob/main/nvrs.toml) + +use crate::error; use serde::{Deserialize, Serialize}; use std::{ collections::BTreeMap, - env, fs, - io::Write, + env, path::{Path, PathBuf}, }; +use tokio::fs; -#[derive(Debug, Clone, Deserialize)] -struct KeysTable { - #[cfg(feature = "github")] - #[serde(default)] - #[serde(skip_serializing_if = "is_empty_string")] - pub github: String, - #[cfg(feature = "gitlab")] - #[serde(default)] - #[serde(skip_serializing_if = "is_empty_string")] - pub gitlab: String, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct Keyfile { - keys: KeysTable, -} - -impl Keyfile { - pub fn get_api_key(&self, api_name: String) -> String { - match api_name.as_str() { - "github" => self.keys.github.clone(), - "gitlab" => self.keys.gitlab.clone(), - _ => String::new(), - } - } +/// main configuration file structure +/// +/// see the [example `nvrs.toml`](https://github.com/adamperkowski/nvrs/blob/main/nvrs.toml) +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Config { + pub __config__: Option, + #[serde(flatten)] + pub packages: BTreeMap, } +/// `__config__` table structure +/// +/// see the [example `nvrs.toml`](https://github.com/adamperkowski/nvrs/blob/main/nvrs.toml) #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ConfigTable { pub oldver: Option, pub newver: Option, - keyfile: Option, - /* proxy: Option, - max_concurrency: Option, - http_timeout: Option, */ + pub keyfile: Option, } +/// package entry structure +/// +/// see the [example `nvrs.toml`](https://github.com/adamperkowski/nvrs/blob/main/nvrs.toml) #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Package { - pub source: String, + source: String, // ex. "github", "aur" #[serde(default)] #[serde(skip_serializing_if = "is_empty_string")] - pub host: String, + host: String, // ex. "gitlab.archlinux.org" + // equivalent to `target` in api::ApiArgs #[cfg(feature = "aur")] #[serde(default)] #[serde(skip_serializing_if = "is_empty_string")] - pub aur: String, + aur: String, #[cfg(feature = "github")] #[serde(default)] #[serde(skip_serializing_if = "is_empty_string")] - pub github: String, + github: String, #[cfg(feature = "gitlab")] #[serde(default)] #[serde(skip_serializing_if = "is_empty_string")] - pub gitlab: String, + gitlab: String, #[serde(default)] #[serde(skip_serializing_if = "is_empty_string")] pub prefix: String, } +// TODO: Package defaults & tests impl Package { - pub fn get_api_arg(&self, api_name: &str) -> Option> { - match api_name { + /// global function to get various API-specific agrs for a package + /// + /// # example + /// ```rust,ignore + /// // package has `source = "github"` * `github = "adamperkowski/nvrs"` specified + /// let args = package.get_api(); + /// + /// assert_eq!(package, ("github", vec!["adamperkowski/nvrs"])) + /// ``` + pub fn get_api(&self) -> (String, Vec) { + let args = match self.source.as_str() { #[cfg(feature = "aur")] - "aur" => Some(vec![self.aur.clone()]), + "aur" => vec![self.aur.clone()], #[cfg(feature = "github")] - "github" => Some(vec![self.github.clone()]), + "github" => vec![self.github.clone()], #[cfg(feature = "gitlab")] - "gitlab" => Some(vec![self.gitlab.clone(), self.host.clone()]), - _ => None, - } - } -} + "gitlab" => vec![self.gitlab.clone(), self.host.clone()], + _ => vec![], + }; -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Config { - pub __config__: Option, - #[serde(flatten)] - pub packages: BTreeMap, + (self.source.clone(), args) + } } -pub fn load(custom_path: Option) -> (Config, PathBuf, Option) { - if custom_path.is_some() { - let custom_path = custom_path.unwrap(); - let config_path = Path::new(&custom_path); +/// global asynchronous function to load all config files +pub async fn load(custom_path: Option) -> error::Result<(Config, PathBuf)> { + if let Some(path) = custom_path { + let config_path = Path::new(&path); if config_path.exists() && config_path.is_file() { - let content = fs::read_to_string(config_path).unwrap_or_default(); - let toml_content: Config = - toml::from_str(&content).expect("failed to read the config file"); - - return ( - toml_content.clone(), - PathBuf::from(config_path), - load_keyfile(toml_content), - ); + let content = fs::read_to_string(config_path).await?; + let toml_content: Config = toml::from_str(&content)?; + + return Ok((toml_content, PathBuf::from(config_path))); } else { - crate::custom_error("specified config file not found", String::new(), ""); + return Err(error::Error::NoConfigSpecified); } } @@ -120,14 +112,14 @@ pub fn load(custom_path: Option) -> (Config, PathBuf, Option) { ); let config_home_path = Path::new(&config_home); - let (content, path_actual) = if config_path.exists() && config_path.is_file() { + let (content, path_final) = if config_path.exists() && config_path.is_file() { ( - fs::read_to_string(config_path).unwrap_or_default(), + fs::read_to_string(config_path).await?, PathBuf::from(config_path), ) } else if config_home_path.exists() && config_home_path.is_file() { ( - fs::read_to_string(config_home_path).unwrap_or_default(), + fs::read_to_string(config_home_path).await?, PathBuf::from(config_home_path), ) } else { @@ -135,54 +127,26 @@ pub fn load(custom_path: Option) -> (Config, PathBuf, Option) { }; if content.is_empty() { - crate::custom_error( - "no config found", - "\nconfig file locations:\n ~/.config/nvrs.toml\n ./nvrs.toml\nmake sure the file is not empty".to_string(), - ""); + return Err(error::Error::NoConfig); } - let toml_content: Config = toml::from_str(&content).expect("error reading the config file"); - - ( - toml_content.clone(), - path_actual, - load_keyfile(toml_content), - ) + Ok((toml::from_str(&content)?, path_final)) } -fn load_keyfile(toml_content: Config) -> Option { - if let Some(config_content) = toml_content.__config__ { - if let Some(keyfile) = config_content.keyfile { - let keyfile_path = Path::new(&keyfile); - let keyfile_content = if keyfile_path.exists() && keyfile_path.is_file() { - fs::read_to_string(keyfile_path).unwrap_or_default() - } else { - String::new() - }; - - if keyfile_content.is_empty() { - crate::custom_error( - "keyfile not found", - "\nmake sure the file is not empty".to_string(), - "exit", - ); - } - - Some(toml::from_str(&keyfile_content).expect("error reading the keyfile")) - } else { - None - } - } else { - None - } +fn is_empty_string(s: &str) -> bool { + s.is_empty() } -pub fn save(config_content: Config, path: PathBuf) -> Result<(), std::io::Error> { - let mut file = fs::File::create(path).unwrap(); - let content = format!("{}\n", toml::to_string(&config_content).unwrap()); - file.write_all(content.as_bytes()) -} +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn loading() { + let config = load(None).await.unwrap(); -fn is_empty_string(value: &str) -> bool { - value.is_empty() + // TODO: here, ref L47 + + assert_eq!(config.1, PathBuf::from("nvrs.toml")); + } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..768b0b9 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,83 @@ +//! [thiserror] implementation + +use thiserror::Error as ThisError; + +const RATE_LIMIT: &str = "we might be getting rate-limited here"; +const CONFIG_PATHS: &str = "config file locations: + ~/.config/nvrs.toml + ./nvrs.toml"; +const NOT_EMPTY: &str = "make sure the file is not empty"; +const EXAMPLE_CONFIG_TABLE: &str = "example: +[__config__] +oldver = \"oldver.json\" +newver = \"newver.json\""; + +#[derive(Debug, ThisError)] +pub enum Error { + #[cfg(feature = "http")] + #[error("request error: {0}")] + RequestError(#[from] reqwest::Error), + + #[error("io error: {0}")] + IOError(#[from] std::io::Error), + + #[error("json parsing error: {0}")] + JSONError(#[from] serde_json::Error), + + #[error("toml parsing error: {0}")] + TOMLError(#[from] toml::de::Error), + + // custom errors + #[error("{0}: request status != OK\n{1}")] + RequestNotOK(String, String), + + #[error("{0}: request returned 430\n{RATE_LIMIT}")] + RequestForbidden(String), + + #[error("{0}: version not found")] + NoVersion(String), + + /// explicitly specified configuration file not found + #[error("specified config file not found")] + NoConfigSpecified, + + /// configuration file not found + #[error("no config found\n{CONFIG_PATHS}\n{NOT_EMPTY}")] + NoConfig, + + /// no `__config__` in the configuration file + #[error("__config__ not specified\n{EXAMPLE_CONFIG_TABLE}")] + NoConfigTable, + + /// keyfile specified in the configuration not found + #[error("specified keyfile not found\n{NOT_EMPTY}")] + NoKeyfile, + + /// no `oldver` or `newver` in `__config__` + #[error("oldver & newver not specified\n{EXAMPLE_CONFIG_TABLE}")] + NoXVer, + + /// verfile version != 2 + #[error("unsupported verfile version\nplease update your verfiles")] + VerfileVer, + + /// package not found in newver + #[error("{0}: package not in newver")] + PkgNotInNewver(String), + + /// source / API not found + #[error("source {0} not found")] + SourceNotFound(String), +} + +pub type Result = std::result::Result; + +#[test] +fn test_error() { + let message = "nvrs died. now why could that be...?"; + let error = Error::from(std::io::Error::other(message)); + assert_eq!( + format!("\"io error: {message}\""), + format!("{:?}", error.to_string()) + ) +} diff --git a/src/keyfile.rs b/src/keyfile.rs new file mode 100644 index 0000000..711e6f9 --- /dev/null +++ b/src/keyfile.rs @@ -0,0 +1,70 @@ +//! operations on keyfiles +//! +//! see the [example `nvrs.toml`](https://github.com/adamperkowski/nvrs/blob/main/nvrs.toml) & [example `keyfile.toml`](https://github.com/adamperkowski/nvrs/blob/main/n_keyfile.toml) + +use crate::{config, error}; +use serde::Deserialize; +use std::path::Path; +use tokio::fs; + +/// keyfile structure +/// +/// see `keyfile` in [crate::config::ConfigTable] +#[derive(Clone, Deserialize)] +pub struct Keyfile { + keys: KeysTable, +} + +/// `[keys]` table structure +/// +/// see the [example `keyfile.toml`](https://github.com/adamperkowski/nvrs/blob/main/n_keyfile.toml) +#[derive(Clone, Deserialize)] +struct KeysTable { + #[cfg(feature = "github")] + #[serde(default)] + #[serde(skip_serializing_if = "config::is_empty_string")] + github: String, + #[cfg(feature = "gitlab")] + #[serde(default)] + #[serde(skip_serializing_if = "config::is_empty_string")] + gitlab: String, +} + +impl Keyfile { + /// returns API key for the specified API name (empty string if not found) + pub async fn get_key(&self, api_name: &str) -> String { + match api_name { + #[cfg(feature = "github")] + "github" => self.keys.github.clone(), + #[cfg(feature = "gitlab")] + "gitlab" => self.keys.gitlab.clone(), + _ => String::new(), + } + } +} + +/// load contents of the specified keyfile +/// +/// see `keyfile` in [crate::config::ConfigTable] +pub async fn load(config_content: Option) -> error::Result> { + if let Some(config_table) = config_content { + if let Some(keyfile) = config_table.keyfile { + let keyfile_path = Path::new(&keyfile); + let keyfile_content = if keyfile_path.exists() && keyfile_path.is_file() { + fs::read_to_string(keyfile_path).await? + } else { + String::new() + }; + + if keyfile_content.is_empty() { + return Err(error::Error::NoKeyfile); + } + + Ok(Some(toml::from_str(&keyfile_content)?)) + } else { + Ok(None) + } + } else { + Ok(None) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..beb3de3 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,83 @@ +//! nvrs - fast new version checker for software releases πŸš¦πŸ¦€ +//! +//!
+//! +//! nvrs is still a WIP +//! +//! new features & bugfixes are being pushed every day +//! +//! you may encounter some issues. please consider [submitting feedback](https://github.com/adamperkowski/nvrs/issues/new/choose) if you do. +//! +//!
+ +pub mod api; +pub mod config; +pub mod error; +pub mod keyfile; +pub mod verfiles; + +/// "core" vars structure +/// +/// # example usage +/// ```rust +/// # tokio_test::block_on(async { +/// use nvrs::*; +/// +/// let config = config::load(None).await.unwrap(); +/// let verfiles = verfiles::load(config.0.__config__.clone()).await.unwrap(); +/// let keyfile = keyfile::load(config.0.__config__.clone()).await.unwrap(); +/// +/// Core { +/// config: config.0, +/// verfiles, +/// client: reqwest::Client::new(), +/// keyfile, +/// }; +/// # }) +/// ``` +pub struct Core { + pub config: config::Config, + pub verfiles: (verfiles::Verfile, verfiles::Verfile), + pub client: reqwest::Client, + pub keyfile: Option, +} + +/// an asynchronous function that package's source and gets the latest release +/// # example usage +/// ```rust,ignore +/// # tokio_test::block_on(async { +/// use nvrs::run_source; +/// +/// let package_name = "nvrs".to_string(); +/// let client = reqwest::Client::new(); +/// +/// run_source((package_name, package), client).await; +/// # }) +/// ``` +/// see [crate::config::Package] for `package` +pub async fn run_source( + package: (String, config::Package), + client: reqwest::Client, + keyfile: Option, +) -> error::Result { + let (source, api_args) = package.1.get_api(); + + if let Some(api) = api::API_LIST.iter().find(|a| a.name == source) { + let api_key = if let Some(keyfile_content) = keyfile { + keyfile_content.get_key(api.name).await + } else { + String::new() + }; + + let args = api::ApiArgs { + request_client: client, + package: package.0, + args: api_args, + api_key, + }; + + Ok((api.func)(args).await?) + } else { + Err(error::Error::SourceNotFound(source)) + } +} diff --git a/src/main.rs b/src/main.rs index fbab430..5091c85 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,245 +1,216 @@ -use clap::Parser; use colored::Colorize; -use config::Keyfile; -use std::sync::Mutex; -use std::time::{SystemTime, UNIX_EPOCH}; +use nvrs::*; -mod api; -pub mod config; -mod verfiles; +mod cli; -lazy_static::lazy_static! { - static ref MSG_NOEXIT: Mutex = Mutex::new(false); +#[tokio::main] +async fn main() -> error::Result<()> { + match init().await { + Ok(core) => { + let res = if core.1.cmp { + compare(core.0).await + } else if core.1.take.is_some() { + take(core.0, core.1.take).await + } else { + sync(core.0, core.1.no_fail).await + }; + + match res { + Ok(_) => (), + Err(e) => pretty_error(&e), + } + } + Err(e) => pretty_error(&e), + } + + Ok(()) } -#[derive(Parser)] -#[command(version, about)] -struct Cli { - #[arg( - short = 'c', - long, - help = "Compare newver with oldver and display differences as updates" - )] - cmp: bool, - - #[arg( - short = 't', - long, - value_name = "packages", - help = "List of packages to update automatically, separated by a comma", - value_delimiter = ',' - )] - take: Option>, - - #[arg( - short = 'n', - long, - value_name = "packages", - help = "List of packages to delete from the config", - value_delimiter = ',' - )] - nuke: Option>, - - #[arg( - long = "config", - value_name = "path", - help = "Override path to the config file" - )] - custom_config: Option, - - #[arg(long, help = "Don't exit the program on recoverable errors")] - no_fail: bool, - - #[arg(long, help = "Display copyright information")] - copyright: bool, +async fn init() -> error::Result<(Core, cli::Cli)> { + let cli = cli::get_args(); + let config = config::load(cli.clone().custom_config).await?; + + let verfiles = verfiles::load(config.0.__config__.clone()).await?; + let keyfile = keyfile::load(config.0.__config__.clone()).await?; + + Ok(( + Core { + config: config.0, + verfiles, + client: reqwest::Client::new(), + keyfile, + }, + cli, + )) } -#[tokio::main] -async fn main() { - let cli = Cli::parse(); +async fn compare(core: Core) -> error::Result<()> { + let (oldver, newver) = core.verfiles; - if cli.no_fail { - *MSG_NOEXIT.lock().unwrap() = true; + for new_pkg in newver.data.data { + if let Some(old_pkg) = oldver.data.data.iter().find(|p| p.0 == &new_pkg.0) { + if old_pkg.1.version != new_pkg.1.version { + println!( + "{} {} {} -> {}", + "*".white().on_black(), + new_pkg.0.blue(), + old_pkg.1.version.red(), + new_pkg.1.version.blue() + ); + } + } else { + println!( + "{} {} {} -> {}", + "*".white().on_black(), + new_pkg.0.blue(), + "NONE".red(), + new_pkg.1.version.green() + ); + } } - if cli.copyright { - let current_year = 1970 - + (SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("time went backwards") - .as_secs() - / (365 * 24 * 60 * 60)); + Ok(()) +} - println!( - "Copyright (c) {} Adam Perkowski\n -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the \"Software\"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions:\n -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software.", - current_year - ); - } else if cli.cmp { - let (config_content, _, _) = config::load(cli.custom_config); - let (oldver, newver) = verfiles::load(config_content.__config__.clone()).unwrap(); +async fn take(core: Core, take_names: Option>) -> error::Result<()> { + let names = take_names.unwrap(); + let config = core.config; + let (mut oldver, newver) = core.verfiles; - for package in newver.data.data { - if let Some(pkg) = oldver.data.data.iter().find(|p| p.0 == &package.0) { - if pkg.1.version != package.1.version { + for package_name in names { + if let Some(new_pkg) = newver.data.data.iter().find(|p| p.0 == &package_name) { + if let Some(old_pkg) = oldver.data.data.iter_mut().find(|p| p.0 == &package_name) { + if old_pkg.1.version != new_pkg.1.version { println!( - "* {} {} -> {}", - package.0.blue(), - pkg.1.version.red(), - package.1.version.green() + "{} {} {} -> {}", + "+".white().on_black(), + package_name.blue(), + old_pkg.1.version.red(), + new_pkg.1.version.green() ); + old_pkg.1.version = new_pkg.1.version.clone(); + old_pkg.1.gitref = new_pkg.1.gitref.clone(); + old_pkg.1.url = new_pkg.1.url.clone(); } } else { println!( - "* {} {} -> {}", - package.0.blue(), + "{} {} {} -> {}", + "+".white().on_black(), + package_name.blue(), "NONE".red(), - package.1.version.green() + new_pkg.1.version.green() ); + oldver.data.data.insert(package_name, new_pkg.1.clone()); } + } else { + return Err(error::Error::PkgNotInNewver(package_name)); } - } else if cli.take.is_some() { - let names = cli.take.unwrap(); - let (config_content, _, _) = config::load(cli.custom_config); - let (mut oldver, newver) = verfiles::load(config_content.__config__.clone()).unwrap(); - - for package_name in names { - if let Some(package) = newver.data.data.iter().find(|p| p.0 == &package_name) { - if let Some(pkg) = oldver.data.data.iter_mut().find(|p| p.0 == &package_name) { - if pkg.1.version != package.1.version { - println!( - "+ {} {} -> {}", - package.0.blue(), - pkg.1.version.red(), - package.1.version.green() - ); + } + + verfiles::save(oldver, true, config.__config__).await +} + +async fn sync(core: Core, no_fail: bool) -> error::Result<()> { + let config = core.config; + let (_, mut newver) = core.verfiles; + + let tasks: Vec<_> = config + .packages + .clone() + .into_iter() + .map(|pkg| tokio::spawn(run_source(pkg, core.client.clone(), core.keyfile.clone()))) + .collect(); + + let mut results = futures::future::join_all(tasks).await; + + for package in config.packages { + match results.remove(0).unwrap() { + Ok(release) => { + if let Some(new_pkg) = newver.data.data.iter_mut().find(|p| p.0 == &package.0) { + let gitref: String; + let tag = if let Some(t) = release.tag.clone() { + gitref = format!("refs/tags/{}", t); + release.tag.unwrap().replacen(&package.1.prefix, "", 1) } else { + gitref = String::new(); + release.name + }; + + if new_pkg.1.version != tag { println!( - "+ {} {} -> {}", + "{} {} {} -> {}", + "|".white().on_black(), package.0.blue(), - pkg.1.version, - package.1.version + new_pkg.1.version.red(), + tag.green() ); + new_pkg.1.version = tag.clone(); + new_pkg.1.gitref = gitref; + new_pkg.1.url = release.url; } - pkg.1.version = package.1.version.clone(); - pkg.1.gitref = package.1.gitref.clone(); - pkg.1.url = package.1.url.clone(); } else { + let gitref: String; + let tag = if let Some(t) = release.tag.clone() { + gitref = format!("refs/tags/{}", t); + release.tag.unwrap().replacen(&package.1.prefix, "", 1) + } else { + gitref = String::new(); + release.name + }; + println!( - "+ {} {} -> {}", + "{} {} {} -> {}", + "|".white().on_black(), package.0.blue(), "NONE".red(), - package.1.version.green() + tag.green() + ); + newver.data.data.insert( + package.0, + verfiles::VerPackage { + version: tag.clone(), + gitref, + url: release.url, + }, ); - oldver.data.data.insert(package_name, package.1.clone()); } - } else { - custom_error("package not in newver: ", package_name, "noexit"); } - } - - verfiles::save(oldver, true, config_content.__config__).unwrap(); - } else if cli.nuke.is_some() { - let names = cli.nuke.unwrap(); - let (mut config_content, config_content_path, _) = config::load(cli.custom_config); - let (mut oldver, mut newver) = verfiles::load(config_content.__config__.clone()).unwrap(); - - for package_name in names { - if config_content.packages.contains_key(&package_name) { - config_content.packages.remove(&package_name); - } else { - custom_error("package not in config: ", package_name.clone(), "noexit"); - } - newver.data.data.remove(&package_name); - oldver.data.data.remove(&package_name); - } - - verfiles::save(newver, false, config_content.__config__.clone()).unwrap(); - verfiles::save(oldver, true, config_content.__config__.clone()).unwrap(); - config::save(config_content, config_content_path).unwrap(); - } else { - let (config_content, _, keyfile) = config::load(cli.custom_config); - let (_, mut newver) = verfiles::load(config_content.__config__.clone()).unwrap(); - - let tasks: Vec<_> = config_content - .packages - .clone() - .into_iter() - .map(|pkg| tokio::spawn(run_source(pkg, keyfile.clone()))) - .collect(); - - let mut results = futures::future::join_all(tasks).await; - - for package in config_content.packages { - let release = results.remove(0).unwrap().unwrap(); - let tag = release.tag_name.replacen(&package.1.prefix, "", 1); - - if let Some(pkg) = newver.data.data.iter_mut().find(|p| p.0 == &package.0) { - if pkg.1.version != tag { - println!( - "| {} {} -> {}", - package.0.blue(), - pkg.1.version.red(), - tag.green() - ); - pkg.1.version = tag; - pkg.1.gitref = format!("refs/tags/{}", release.tag_name); - pkg.1.url = release.html_url; + Err(e) => { + pretty_error(&e); + if !no_fail { + return Err(e); } - } else { - println!("| {} {} -> {}", package.0.blue(), "NONE".red(), tag.green()); - newver.data.data.insert( - package.0, - verfiles::Package { - version: tag, - gitref: format!("refs/tags/{}", release.tag_name), - url: release.html_url, - }, - ); } - } - - verfiles::save(newver, false, config_content.__config__).unwrap(); + }; } -} -async fn run_source( - package: (String, config::Package), - keyfile: Option, -) -> Option { - let source = package.1.source.clone(); - if let Some(api_used) = api::API_LIST.iter().find(|a| a.name == source) { - let api_key = if let Some(k) = keyfile { - k.get_api_key(source) - } else { - String::new() - }; + verfiles::save(newver, false, config.__config__).await +} - Some( - (api_used.func)( - package.0, - package.1.get_api_arg(api_used.name).unwrap(), - api_key, - ) - .await?, - ) +fn pretty_error(err: &error::Error) { + let mut lines: Vec = err + .to_string() + .lines() + .map(|line| line.to_string()) + .collect(); + let first = lines.remove(0); + let first_split = first.split_once(':').unwrap_or(("", &first)); + if first_split.0.is_empty() { + println!("{} {}", "!".red().bold().on_black(), first_split.1.red()); } else { - custom_error("api not found: ", source, ""); - None + println!( + "{} {}:{}", + "!".red().bold().on_black(), + first_split.0, + first_split.1.red() + ); + } + for line in lines { + println!("{} {}", "!".red().on_black(), line) } } -pub fn custom_error(message: &'static str, message_ext: String, override_exit: &str) { - println!("! {}{}", message.red(), message_ext.replace("\n", "\n ")); - - if override_exit != "noexit" && !*MSG_NOEXIT.lock().unwrap() { - std::process::exit(1); - } +#[tokio::test] +async fn core_initializing() { + assert!(init().await.is_ok()) } diff --git a/src/verfiles.rs b/src/verfiles.rs index 998552a..75cafe9 100644 --- a/src/verfiles.rs +++ b/src/verfiles.rs @@ -1,22 +1,29 @@ +//! operations on version files +//! +//! see `newver` & `oldver` in [crate::config::ConfigTable] + use serde::{Deserialize, Serialize}; -use std::{collections::BTreeMap, fs, io::Write, path::Path}; +use std::{collections::BTreeMap, path::Path}; +use tokio::{fs, io::AsyncWriteExt}; -use crate::config::ConfigTable; +use crate::{config, error}; +// verfiles get created from this const TEMPLATE: &str = r#"{ "version": 2, "data": {} } "#; -const CONFIG_NONE_M: &str = "__config__ not specified\nexample:"; -const XVER_NONE_M: &str = "oldver & newver not specified\nexample:"; -const CONFIG_NONE_E: &str = "\n[__config__] -oldver = \"oldver.json\" -newver = \"newver.json\""; +/// main data structure +#[derive(Serialize, Deserialize)] +pub struct VerData { + pub data: BTreeMap, +} +/// package entry structure #[derive(Clone, Serialize, Deserialize)] -pub struct Package { +pub struct VerPackage { pub version: String, #[serde(default)] pub gitref: String, @@ -24,68 +31,62 @@ pub struct Package { pub url: String, } -#[derive(Clone, Serialize, Deserialize)] -pub struct Data { - pub data: BTreeMap, -} - -#[derive(Clone, Serialize, Deserialize)] +/// file structure +#[derive(Serialize, Deserialize)] pub struct Verfile { version: u8, #[serde(flatten)] - pub data: Data, + pub data: VerData, } -pub fn load(config_table: Option) -> Option<(Verfile, Verfile)> { +/// load the verfiles specified in [crate::config::ConfigTable] +pub async fn load(config_table: Option) -> error::Result<(Verfile, Verfile)> { if config_table.is_none() { - crate::custom_error(CONFIG_NONE_M, CONFIG_NONE_E.to_string(), ""); + return Err(error::Error::NoConfigTable); } let config_table = config_table.unwrap(); if config_table.oldver.is_some() && config_table.newver.is_some() { - let oldver_path = Path::new(config_table.oldver.as_ref().unwrap()); - let newver_path = Path::new(config_table.newver.as_ref().unwrap()); - let oldver = load_file(oldver_path); - let newver = load_file(newver_path); + let oldver = load_file(Path::new(config_table.oldver.as_ref().unwrap())).await?; + let newver = load_file(Path::new(config_table.newver.as_ref().unwrap())).await?; if oldver.version != 2 || newver.version != 2 { - crate::custom_error( - "unsupported verfile version", - "\nplease update your verfiles".to_string(), - "", - ); + return Err(error::Error::VerfileVer); } - Some((oldver, newver)) + Ok((oldver, newver)) } else { - crate::custom_error(XVER_NONE_M, CONFIG_NONE_E.to_string(), ""); - None + Err(error::Error::NoXVer) } } -pub fn save( +/// save changes to the verfiles +pub async fn save( verfile: Verfile, - oldver: bool, - config_table: Option, -) -> Result<(), std::io::Error> { + is_oldver: bool, + config_table: Option, +) -> error::Result<()> { let config_table = config_table.unwrap(); - let path = if oldver { + let path = if is_oldver { Path::new(config_table.oldver.as_ref().unwrap()) } else { Path::new(config_table.newver.as_ref().unwrap()) }; - let mut file = fs::File::create(path).unwrap(); - let content = format!("{}\n", serde_json::to_string_pretty(&verfile).unwrap()); - file.write_all(content.as_bytes()) + let mut file = fs::File::create(path).await?; + let content = format!("{}\n", serde_json::to_string_pretty(&verfile)?); + + Ok(file.write_all(content.as_bytes()).await?) } -fn load_file(path: &Path) -> Verfile { +async fn load_file(path: &Path) -> error::Result { if !path.exists() { - let mut file = fs::File::create(path).unwrap(); - file.write_all(TEMPLATE.as_bytes()).unwrap(); + let mut file = fs::File::create(path).await?; + file.write_all(TEMPLATE.as_bytes()).await?; } - let content = fs::read_to_string(path).unwrap(); + let content = fs::read_to_string(path).await?; - serde_json::from_str(&content).expect("failed to read oldver") + Ok(serde_json::from_str(&content)?) } + +// TODO: tests From 705ed426560b5e4b3285bb7c09c6b9a25558288c Mon Sep 17 00:00:00 2001 From: adamperkowski Date: Mon, 25 Nov 2024 00:20:50 +0000 Subject: [PATCH 04/14] changelog for c0021f0 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8be0ec..32289f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,17 @@ All notable changes to nvrs will be documented in this file. - (*verfile*) allow missing gitref & url ([b93216d](https://github.com/adamperkowski/nvrs/commit/b93216d5146a672897e11938668e05cfa859cfac)) +### βš™οΈ Refactoring + +- (*codebase*) [**breaking**] move internal logic to `lib` ([#4](https://github.com/adamperkowski/nvrs/issues/4)) ([c0021f0](https://github.com/adamperkowski/nvrs/commit/c0021f0a4e02791802fba9ba6bca5486f825ee4e)) + ### πŸ“š Documentation - (*git-cliff*) add `UI/UX` ([42727ad](https://github.com/adamperkowski/nvrs/commit/42727ad6bd020ecee06e93017e7e5b68851c01d3)) - (*config*) fix the package name (alpm -> mkinitcpio) ([1327516](https://github.com/adamperkowski/nvrs/commit/132751692941f5e1e2cce188d545f3ee421dad46)) - better banner ([a4718b6](https://github.com/adamperkowski/nvrs/commit/a4718b60505d26c2e262b70d77160b475b8f2348)) - (*dependabot*) change cargo commit message ([90d50ab](https://github.com/adamperkowski/nvrs/commit/90d50ab0fd6cd4964408796e2f75affeb539923b)) +- 🚦 ([f2e22b6](https://github.com/adamperkowski/nvrs/commit/f2e22b6c8daece310080a8e32d183e0f6ef3e3f0)) ### 🧩 UI/UX From 15b75d99667a4c52d0d9b093704aa02ca4d35e3e Mon Sep 17 00:00:00 2001 From: Adam Perkowski Date: Mon, 25 Nov 2024 11:26:50 +0100 Subject: [PATCH 05/14] fix: `--nuke` not working Signed-off-by: Adam Perkowski --- src/config.rs | 11 ++++++++++- src/error.rs | 7 +++++++ src/lib.rs | 4 ++-- src/main.rs | 32 +++++++++++++++++++++++++++++--- 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/config.rs b/src/config.rs index b4e1d05..4d434fe 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,7 +9,7 @@ use std::{ env, path::{Path, PathBuf}, }; -use tokio::fs; +use tokio::{fs, io::AsyncWriteExt}; /// main configuration file structure /// @@ -133,6 +133,15 @@ pub async fn load(custom_path: Option) -> error::Result<(Config, PathBuf Ok((toml::from_str(&content)?, path_final)) } +// FIXME: this nukes all the comments +/// global asynchronous function to save the config file +pub async fn save(config_content: Config, path: PathBuf) -> error::Result<()> { + let mut file = fs::File::create(path).await?; + let content = format!("{}\n", toml::to_string(&config_content)?); + file.write_all(content.as_bytes()).await?; + Ok(()) +} + fn is_empty_string(s: &str) -> bool { s.is_empty() } diff --git a/src/error.rs b/src/error.rs index 768b0b9..95a3166 100644 --- a/src/error.rs +++ b/src/error.rs @@ -27,6 +27,9 @@ pub enum Error { #[error("toml parsing error: {0}")] TOMLError(#[from] toml::de::Error), + #[error("toml parsing error: {0}")] + TOMLErrorSer(#[from] toml::ser::Error), + // custom errors #[error("{0}: request status != OK\n{1}")] RequestNotOK(String, String), @@ -65,6 +68,10 @@ pub enum Error { #[error("{0}: package not in newver")] PkgNotInNewver(String), + /// package not found in config + #[error("{0}: package not in config")] + PkgNotInConfig(String), + /// source / API not found #[error("source {0} not found")] SourceNotFound(String), diff --git a/src/lib.rs b/src/lib.rs index beb3de3..8e42427 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,7 +28,7 @@ pub mod verfiles; /// let keyfile = keyfile::load(config.0.__config__.clone()).await.unwrap(); /// /// Core { -/// config: config.0, +/// config, /// verfiles, /// client: reqwest::Client::new(), /// keyfile, @@ -36,7 +36,7 @@ pub mod verfiles; /// # }) /// ``` pub struct Core { - pub config: config::Config, + pub config: (config::Config, std::path::PathBuf), pub verfiles: (verfiles::Verfile, verfiles::Verfile), pub client: reqwest::Client, pub keyfile: Option, diff --git a/src/main.rs b/src/main.rs index 5091c85..d49f216 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,8 @@ async fn main() -> error::Result<()> { compare(core.0).await } else if core.1.take.is_some() { take(core.0, core.1.take).await + } else if core.1.nuke.is_some() { + nuke(core.0, core.1.nuke, core.1.no_fail).await } else { sync(core.0, core.1.no_fail).await }; @@ -35,7 +37,7 @@ async fn init() -> error::Result<(Core, cli::Cli)> { Ok(( Core { - config: config.0, + config, verfiles, client: reqwest::Client::new(), keyfile, @@ -107,11 +109,35 @@ async fn take(core: Core, take_names: Option>) -> error::Result<()> } } - verfiles::save(oldver, true, config.__config__).await + verfiles::save(oldver, true, config.0.__config__).await +} + +async fn nuke(core: Core, nuke_names: Option>, no_fail: bool) -> error::Result<()> { + let names = nuke_names.unwrap(); + let mut config_content = core.config.0; + let (mut oldver, mut newver) = core.verfiles; + + for package_name in names { + if config_content.packages.contains_key(&package_name) { + config_content.packages.remove(&package_name); + } else if no_fail { + pretty_error(&error::Error::PkgNotInConfig(package_name.clone())); + } else { + return Err(error::Error::PkgNotInConfig(package_name)); + } + newver.data.data.remove(&package_name); + oldver.data.data.remove(&package_name); + } + + verfiles::save(newver, false, config_content.__config__.clone()).await?; + verfiles::save(oldver, true, config_content.__config__.clone()).await?; + config::save(config_content, core.config.1).await?; + + Ok(()) } async fn sync(core: Core, no_fail: bool) -> error::Result<()> { - let config = core.config; + let config = core.config.0; let (_, mut newver) = core.verfiles; let tasks: Vec<_> = config From 0ee83eb785f939780c8e07920c1f98a8a258d158 Mon Sep 17 00:00:00 2001 From: Adam Perkowski Date: Mon, 25 Nov 2024 13:01:49 +0100 Subject: [PATCH 06/14] feat(take): `ALL` functionality Signed-off-by: Adam Perkowski --- man/nvrs.1 | 4 ++-- src/cli.rs | 4 ++-- src/main.rs | 28 +++++++++++++++++----------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/man/nvrs.1 b/man/nvrs.1 index ca90deb..48a9f83 100644 --- a/man/nvrs.1 +++ b/man/nvrs.1 @@ -19,11 +19,11 @@ Compare newver with oldver and display differences as updates .TP \fB\-t\fR, \fB\-\-take\fR \fI\fR -List of packages to update automatically, separated by a comma +Comma-separated list of packages to update automatically (use `ALL` for all) .TP \fB\-n\fR, \fB\-\-nuke\fR \fI\fR -List of packages to delete from the config, separated by a comma +Comma-separated list of packages to delete from the config .TP \fB\-\-config\fR \fI\fR diff --git a/src/cli.rs b/src/cli.rs index ed28248..5facb62 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -25,7 +25,7 @@ pub struct Cli { short = 't', long, value_name = "packages", - help = "List of packages to update automatically, separated by a comma", + help = "Comma-separated list of packages to update automatically (use `ALL` for all)", value_delimiter = ',' )] pub take: Option>, @@ -34,7 +34,7 @@ pub struct Cli { short = 'n', long, value_name = "packages", - help = "List of packages to delete from the config", + help = "Comma-separated list of packages to delete from the config", value_delimiter = ',' )] pub nuke: Option>, diff --git a/src/main.rs b/src/main.rs index d49f216..053e4d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,20 +79,26 @@ async fn take(core: Core, take_names: Option>) -> error::Result<()> let config = core.config; let (mut oldver, newver) = core.verfiles; - for package_name in names { - if let Some(new_pkg) = newver.data.data.iter().find(|p| p.0 == &package_name) { - if let Some(old_pkg) = oldver.data.data.iter_mut().find(|p| p.0 == &package_name) { - if old_pkg.1.version != new_pkg.1.version { + let packages_to_update = if names.contains(&"ALL".to_string()) { + newver.data.data.keys().cloned().collect() + } else { + names + }; + + for package_name in packages_to_update { + if let Some(new_pkg) = newver.data.data.get(&package_name) { + if let Some(old_pkg) = oldver.data.data.get_mut(&package_name) { + if old_pkg.version != new_pkg.version { println!( "{} {} {} -> {}", "+".white().on_black(), package_name.blue(), - old_pkg.1.version.red(), - new_pkg.1.version.green() + old_pkg.version.red(), + new_pkg.version.green() ); - old_pkg.1.version = new_pkg.1.version.clone(); - old_pkg.1.gitref = new_pkg.1.gitref.clone(); - old_pkg.1.url = new_pkg.1.url.clone(); + old_pkg.version = new_pkg.version.clone(); + old_pkg.gitref = new_pkg.gitref.clone(); + old_pkg.url = new_pkg.url.clone(); } } else { println!( @@ -100,9 +106,9 @@ async fn take(core: Core, take_names: Option>) -> error::Result<()> "+".white().on_black(), package_name.blue(), "NONE".red(), - new_pkg.1.version.green() + new_pkg.version.green() ); - oldver.data.data.insert(package_name, new_pkg.1.clone()); + oldver.data.data.insert(package_name, new_pkg.clone()); } } else { return Err(error::Error::PkgNotInNewver(package_name)); From 6b4d0083153c218df8574b2539096f4ee1493792 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 18:35:34 +0100 Subject: [PATCH 07/14] chore(deps): bump rustls from 0.23.17 to 0.23.18 (#6) Bumps [rustls](https://github.com/rustls/rustls) from 0.23.17 to 0.23.18. - [Release notes](https://github.com/rustls/rustls/releases) - [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md) - [Commits](https://github.com/rustls/rustls/compare/v/0.23.17...v/0.23.18) --- updated-dependencies: - dependency-name: rustls dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index afa9340..385077a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1045,9 +1045,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.17" +version = "0.23.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f1a745511c54ba6d4465e8d5dfbd81b45791756de28d4981af70d6dca128f1e" +checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f" dependencies = [ "once_cell", "rustls-pki-types", From 2ea052dfcf870ec2ec58be2f5e39acb873cc783b Mon Sep 17 00:00:00 2001 From: Adam Perkowski Date: Mon, 25 Nov 2024 18:36:00 +0100 Subject: [PATCH 08/14] chore(release): prepare for v0.1.4 Signed-off-by: Adam Perkowski --- CHANGELOG.md | 7 ++++++- Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32289f6..06f4d5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,16 @@ All notable changes to nvrs will be documented in this file. -## [upstream] +## [0.1.4] - 2024-11-25 + +### πŸš€ Features + +- (*take*) `ALL` functionality ([0ee83eb](https://github.com/adamperkowski/nvrs/commit/0ee83eb785f939780c8e07920c1f98a8a258d158)) ### πŸ› Bug Fixes - (*verfile*) allow missing gitref & url ([b93216d](https://github.com/adamperkowski/nvrs/commit/b93216d5146a672897e11938668e05cfa859cfac)) +- `--nuke` not working ([15b75d9](https://github.com/adamperkowski/nvrs/commit/15b75d99667a4c52d0d9b093704aa02ca4d35e3e)) ### βš™οΈ Refactoring diff --git a/Cargo.lock b/Cargo.lock index 385077a..799090d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -820,7 +820,7 @@ dependencies = [ [[package]] name = "nvrs" -version = "0.1.4-pre1" +version = "0.1.4" dependencies = [ "clap", "colored", diff --git a/Cargo.toml b/Cargo.toml index 0dc1577..a09011f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nvrs" -version = "0.1.4-pre1" +version = "0.1.4" authors = ["Adam Perkowski "] license = "MIT" description = "🚦 fast new version checker for software releases πŸ¦€" From cd0bd7269f35ccb559f81abfda62c69ae06bce79 Mon Sep 17 00:00:00 2001 From: Adam Perkowski Date: Tue, 26 Nov 2024 19:26:22 +0100 Subject: [PATCH 09/14] docs(README): add installation & usage instructions Signed-off-by: Adam Perkowski --- .gitignore | 1 + README.md | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++--- linux | 51 ++++++++++++++++++++ nvrs.tape | 61 ++++++++++++++++++++++++ 4 files changed, 242 insertions(+), 7 deletions(-) create mode 100644 linux create mode 100644 nvrs.tape diff --git a/.gitignore b/.gitignore index d40e8fa..ebc21c9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ oldver.json keyfile.toml *_old *.old +*.gif diff --git a/README.md b/README.md index a4fcb09..4294cd3 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@
# nvrs -🚦 fast new version checker for software releases πŸ¦€
-[nvchecker](https://github.com/lilydjwg/nvchecker) rewritten in Rust +🚦 fast new version checker for software releases πŸ¦€ -![Build Status](https://img.shields.io/github/actions/workflow/status/adamperkowski/nvrs/rust.yml?style=for-the-badge&labelColor=%23a8127d&color=%23336795) ![docs.rs](https://img.shields.io/docsrs/nvrs?style=for-the-badge&labelColor=%23a8127d&color=%23336795)
-![GitHub Contributors](https://img.shields.io/github/contributors-anon/adamperkowski/nvrs?style=for-the-badge&labelColor=%23a8127d&color=%23336795) ![GitHub Repo Size](https://img.shields.io/github/repo-size/adamperkowski/nvrs?style=for-the-badge&labelColor=%23a8127d&color=%23336795) ![Repo Created At](https://img.shields.io/github/created-at/adamperkowski/nvrs?style=for-the-badge&labelColor=%23a8127d&color=%23336795) +![Build Status](https://img.shields.io/github/actions/workflow/status/adamperkowski/nvrs/rust.yml?style=for-the-badge&labelColor=%23a8127d&color=%23336795) [![docs.rs](https://img.shields.io/docsrs/nvrs?style=for-the-badge&labelColor=%23a8127d&color=%23336795)](#documentation)
+[![GitHub Contributors](https://img.shields.io/github/contributors-anon/adamperkowski/nvrs?style=for-the-badge&labelColor=%23a8127d&color=%23336795)](https://github.com/adamperkowski/nvrs/graphs/contributors) ![GitHub Repo Size](https://img.shields.io/github/repo-size/adamperkowski/nvrs?style=for-the-badge&labelColor=%23a8127d&color=%23336795) ![Repo Created At](https://img.shields.io/github/created-at/adamperkowski/nvrs?style=for-the-badge&labelColor=%23a8127d&color=%23336795) ![banner](/banner.webp) @@ -18,8 +17,11 @@ new features & bugfixes are being pushed every day you may encounter some issues. please consider [submitting feedback](https://github.com/adamperkowski/nvrs/issues/new/choose) if you do. ## Features +### [nvchecker](https://github.com/lilydjwg/nvchecker) compatibility +check the [release notes](https://github.com/adamperkowski/nvrs/releases) for compatibility updates + ### Speed -ka-chow +ka-chow | command | time per **updated** package | details | |---------------|------------------------------|--------------------------------------------------------| @@ -28,12 +30,132 @@ you may encounter some issues. please consider [submitting feedback](https://git | `nvrs --take` | ~ 0.001s | depends on disk speed | ### Sources -**WIP** - - `aur` - `github` - `gitlab` (with custom hosts) +## Installation +Packaging status + +
+Arch Linux + +[nvrs](https://aur.archlinux.org/packages/nvrs) is available as a package in the [AUR](https://aur.archlinux.org).
+you can install it with your preferred [AUR helper](https://wiki.archlinux.org/title/AUR_helpers), example: + +```sh +paru -S nvrs +``` + +or manually: + +```sh +git clone https://aur.archlinux.org/nvrs.git +cd nvrs +makepkg -si +``` + +
+ +
+Cargo + +[nvrs](https://crates.io/crates/nvrs) can be installed via [Cargo](https://doc.rust-lang.org/cargo) with: + +```sh +cargo install nvrs +``` + +note that crates installed using `cargo install` require manual updating with `cargo install --force`. + +
+ +
+Manual + +1. Download the latest binary from [GitHub's release page](https://github.com/adamperkowski/nvrs/releases/latest) +2. Allow execution +```sh +chmod +x nvrs +``` +3. Move the file to a directory in `$PATH` (using `/usr/bin` as an example) +```sh +sudo mv nvrs /usr/bin/nvrs +``` + +
+ +## Usage +nvrs relies on a configuration file. see [configuration](#configuration). + +Packaging status + +the core commands are: +- `nvrs` - fetches latest versions of defined packages +- `nvrs --cmp` - compares newver with oldver and displays differences +- `nvrs --take` - automatically updates oldver. takes in a comma-separated list of package names (`ALL` for all packages) +- `nvrs --nuke` - deletes packages from all files. takes in a comma-separated list of names (yes, just like a hitman) +- the `--no-fail` flag - as the name suggests, specifying this will make nvrs not exit on recoverable errors + +### Example usage +```sh +# download the example configuration file +curl -L 'https://github.com/adamperkowski/nvrs/raw/main/nvrs.toml' -o nvrs.toml + +# fetch latest package versions (should return `NONE -> version` for all packages) +nvrs --no-fail + +# compare them to latest known versions (should also return `NONE -> version`) +nvrs -c + +# update the known versions +nvrs -t ALL +``` + +## Configuration +nvrs relies on a configuration file ([example](/nvrs.toml)) containing basic settings, such as `oldver`, `newver` & `keyfile` paths, as well as [package entries](#package-entries). supported config paths: +- `$XDG_CONFIG_HOME/nvrs.toml` (`~/.config/nvrs.toml` if the variable is not set) +- `./nvrs.toml` +- custom paths set with `nvrs --config` + +### `__config__` table +this configures the behavior of nvrs. see the [example config](/nvrs.toml#L7-L10). + +available fields: + +| name | description | type | required | +|-----------|-----------------------------------------------------------------|--------|----------| +| `oldver` | path to the `oldver` file | string | βœ”οΈ | +| `newver` | path to the `newver` file | string | βœ”οΈ | +| `keyfile` | path to a keyfile (see [keyfile structure](#keyfile-structure)) | string | ❌ | + +### Package entries + +[example](/nvrs.toml#L12-L15) + +package entries are custom entries in the main config file. they contain values such as: + +| name | description | type | required | custom | +|-------------|---------------------------------------------------------------------------|--------|----------|--------| +| `source` | see [sources](#sources) | string | βœ”οΈ | ❌ | +| source name | the "target". eg. repo path for `github` | string | βœ”οΈ | βœ”οΈ | +| `host` | domain name the source is hosted on | string | ❌ | ❌ | +| `prefix` | the prefix used in releases / tags
example: `v` for tags like `v0.1.0` | string | ❌ | ❌ | + +### Keyfile structure +this file contains API keys for various [sources](#sources). example can be found [here](/n_keyfile.toml). + +```toml +[keys] +github = "your_secret_github_api_key_that_you_shouldnt_push_to_a_public_nor_a_private_remote_repo_because_there_will_definitely_be_serious_consequences_sooner_or_later_if_you_do_trust_me_just_dont" +gitlab = "remember_to_replace_the_example_values_here_here_with_your_actual_keys_otherwise_it_wont_work_but_dont_push_keyfiles_to_remote_repos" +``` + +"I think that example value is not long enough" - orhun + +## Documentation +the nvrs library documentation can be found at [docs.rs/nvrs](https://docs.rs/nvrs/latest/nvrs) + ## Credits - [依云](https://github.com/lilydjwg) | the original [nvchecker](https://github.com/lilydjwg/nvchecker) - [orhun](https://github.com/orhun) | the idea diff --git a/linux b/linux new file mode 100644 index 0000000..3c412fb --- /dev/null +++ b/linux @@ -0,0 +1,51 @@ +#!/bin/sh -e + +# Prevent execution if this script was only partially downloaded +{ +rc='\033[0m' +red='\033[0;31m' + +check() { + exit_code=$1 + message=$2 + + if [ "$exit_code" -ne 0 ]; then + printf '%sERROR: %s%s\n' "$red" "$message" "$rc" + exit 1 + fi + + unset exit_code + unset message +} + +findArch() { + case "$(uname -m)" in + x86_64|amd64) arch="x86_64" ;; + aarch64|arm64) arch="aarch64" ;; + *) check 1 "Unsupported architecture" + esac +} + +getUrl() { + case "${arch}" in + x86_64) echo "https://github.com/ChrisTitusTech/linutil/releases/latest/download/linutil";; + *) echo "https://github.com/ChrisTitusTech/linutil/releases/latest/download/linutil-${arch}";; + esac +} + +findArch +temp_file=$(mktemp) +check $? "Creating the temporary file" + +curl -fsL "$(getUrl)" -o "$temp_file" +check $? "Downloading linutil" + +chmod +x "$temp_file" +check $? "Making linutil executable" + +"$temp_file" +check $? "Executing linutil" + +rm -f "$temp_file" +check $? "Deleting the temporary file" +} # End of wrapping diff --git a/nvrs.tape b/nvrs.tape new file mode 100644 index 0000000..0159781 --- /dev/null +++ b/nvrs.tape @@ -0,0 +1,61 @@ +# VHS +# https://github.com/charmbracelet/vhs + +Output nvrs.gif + +Require echo + +Set Shell "zsh" +Set FontSize 32 +Set Width 1280 +Set Height 720 +Set TypingSpeed 0ms +Set Theme { "black": "#000D19", "red": "#7D0C0C", "green": "#0C7D45", "yellow": "#7D750C", "blue": "#0C567E", "magenta": "#6E1D57", "cyan": "#12748F", "white": "#A3A3A3", "brightBlack": "#000D19", "brightRed": "#951313", "brightGreen": "#16A85F", "brightYellow": "#ABA119", "brightBlue": "#1A76A8", "brightMagenta": "#A01576", "brightCyan": "#188AA9", "brightWhite": "#A3A3A3", "background": "#00050D", "foreground": "#A3A3A3", "selection": "#6E1D57", "cursor": "#A3A3A3" } + +Hide +Type "source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.plugin.zsh" +Enter +Type "source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.plugin.zsh" +Enter +Ctrl+L +Sleep 100ms +Show + +Set TypingSpeed 30ms + +Type "1. fetch latest versions of defined packages" +Sleep 1s +Ctrl+U +Sleep 500ms +Type "nvrs" +Sleep 500ms +Enter + +Sleep 1.5s +Ctrl+L + +Type "2. compare them to latest known versions" +Sleep 1s +Ctrl+U +Sleep 500ms +Type "nvrs --cmp" +Sleep 500ms +Enter + +Sleep 1.5s +Ctrl+L + +Type "3. update the known versions" +Sleep 1s +Ctrl+U +Sleep 500ms +Type "nvrs --take ALL" +Sleep 500ms +Enter + +Sleep 2s + +Enter +Type "4. go on github.com/adamperkowski/nvrs & leave a star" + +Sleep 4s From 843141248520b7a784cae15c0571cd23e68d277e Mon Sep 17 00:00:00 2001 From: Adam Perkowski Date: Wed, 27 Nov 2024 03:09:57 +0100 Subject: [PATCH 10/14] feat: `use_max_tag` functionality Signed-off-by: Adam Perkowski --- README.md | 13 +++---- nvrs.toml | 11 ++++-- src/api/aur.rs | 3 +- src/api/github.rs | 90 ++++++++++++++++++++++++++++++++--------------- src/api/gitlab.rs | 71 ++++++++++++++++++++++++++----------- src/api/mod.rs | 1 + src/config.rs | 2 ++ src/lib.rs | 1 + 8 files changed, 132 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 4294cd3..d88f6be 100644 --- a/README.md +++ b/README.md @@ -135,12 +135,13 @@ available fields: package entries are custom entries in the main config file. they contain values such as: -| name | description | type | required | custom | -|-------------|---------------------------------------------------------------------------|--------|----------|--------| -| `source` | see [sources](#sources) | string | βœ”οΈ | ❌ | -| source name | the "target". eg. repo path for `github` | string | βœ”οΈ | βœ”οΈ | -| `host` | domain name the source is hosted on | string | ❌ | ❌ | -| `prefix` | the prefix used in releases / tags
example: `v` for tags like `v0.1.0` | string | ❌ | ❌ | +| name | description | type | required | custom | +|---------------|---------------------------------------------------------------------------|--------|----------|--------| +| `source` | see [sources](#sources) | string | βœ”οΈ | ❌ | +| source name | the "target". eg. repo path for `github` | string | βœ”οΈ | βœ”οΈ | +| `host` | domain name the source is hosted on | string | ❌ | ❌ | +| `prefix` | the prefix used in releases / tags
example: `v` for tags like `v0.1.0` | string | ❌ | ❌ | +| `use_max_tag` | use max git tag instead of the latest release | bool | ❌ | ❌ | ### Keyfile structure this file contains API keys for various [sources](#sources). example can be found [here](/n_keyfile.toml). diff --git a/nvrs.toml b/nvrs.toml index 0355018..62394c1 100644 --- a/nvrs.toml +++ b/nvrs.toml @@ -14,12 +14,17 @@ source = "github" github = "hyprutils/hyprgui" prefix = "v" +[hyprwall] +source = "aur" +aur = "hyprwall" + [mkinitcpio] source = "gitlab" host = "gitlab.archlinux.org" gitlab = "archlinux/mkinitcpio/mkinitcpio" prefix = "v" -[hyprwall] -source = "aur" -aur = "hyprwall" +[rustup] +source = "github" +github = "rust-lang/rustup" +use_max_tag = true diff --git a/src/api/aur.rs b/src/api/aur.rs index b9eab93..796ff01 100644 --- a/src/api/aur.rs +++ b/src/api/aur.rs @@ -39,10 +39,11 @@ pub fn get_latest(args: api::ApiArgs) -> api::ReleaseFuture { async fn request_test() { let package = "permitter".to_string(); let args = api::ApiArgs { + request_client: reqwest::Client::new(), package: package.clone(), + use_max_tag: None, args: vec![package], api_key: String::new(), - request_client: reqwest::Client::new(), }; assert!(get_latest(args).await.is_ok()); diff --git a/src/api/github.rs b/src/api/github.rs index 5c87000..11e95f9 100644 --- a/src/api/github.rs +++ b/src/api/github.rs @@ -1,6 +1,10 @@ -use reqwest::header::{HeaderValue, ACCEPT, AUTHORIZATION}; +use reqwest::{ + header::{HeaderValue, ACCEPT, AUTHORIZATION}, + Response, +}; +use serde_json::Value; -use crate::api; +use crate::{api, error}; #[derive(serde::Deserialize)] struct GitHubResponse { @@ -10,46 +14,74 @@ struct GitHubResponse { pub fn get_latest(args: api::ApiArgs) -> api::ReleaseFuture { Box::pin(async move { - let url = format!( - "https://api.github.com/repos/{}/releases/latest", - args.args[0] - ); - let mut headers = api::setup_headers(); - headers.insert( - ACCEPT, - HeaderValue::from_static("application/vnd.github+json"), - ); - headers.insert( - "X-GitHub-Api-Version", - HeaderValue::from_static("2022-11-28"), - ); - if !args.api_key.is_empty() { - let bearer = format!("Bearer {}", args.api_key); - headers.insert(AUTHORIZATION, HeaderValue::from_str(&bearer).unwrap()); - } - let client = args.request_client; + let repo_url = format!("https://api.github.com/repos/{}", args.args[0]); + + if args.use_max_tag.is_some_and(|x| x) { + let url = format!("{}/tags", repo_url); + let result = request(url, &args).await?; + let json: Value = result.json().await?; - let result = client.get(url).headers(headers).send().await?; - api::match_statuscode(&result.status(), args.package)?; + let max_tag = json + .get(0) + .unwrap() + .get("name") + .unwrap() + .to_string() + .replace("\"", ""); - let json: GitHubResponse = result.json().await?; + Ok(api::Release { + name: max_tag.clone(), + tag: Some(max_tag.clone()), + url: format!( + "https://github.com/{}/releases/tag/{}", + args.args[0], max_tag + ), + }) + } else { + let url = format!("{}/releases/latest", repo_url); + let result = request(url, &args).await?; + let json: GitHubResponse = result.json().await?; - Ok(api::Release { - name: json.tag_name.clone(), - tag: Some(json.tag_name), - url: json.html_url, - }) + Ok(api::Release { + name: json.tag_name.clone(), + tag: Some(json.tag_name), + url: json.html_url, + }) + } }) } +async fn request(url: String, args: &api::ApiArgs) -> error::Result { + let mut headers = api::setup_headers(); + headers.insert( + ACCEPT, + HeaderValue::from_static("application/vnd.github+json"), + ); + headers.insert( + "X-GitHub-Api-Version", + HeaderValue::from_static("2022-11-28"), + ); + if !args.api_key.is_empty() { + let bearer = format!("Bearer {}", args.api_key); + headers.insert(AUTHORIZATION, HeaderValue::from_str(&bearer).unwrap()); + } + let client = &args.request_client; + + let result = client.get(url).headers(headers).send().await?; + api::match_statuscode(&result.status(), args.package.clone())?; + + Ok(result) +} + #[tokio::test] async fn request_test() { let package = "nvrs".to_string(); let args = api::ApiArgs { + request_client: reqwest::Client::new(), package: package.clone(), + use_max_tag: None, args: vec![format!("adamperkowski/{}", package)], api_key: String::new(), - request_client: reqwest::Client::new(), }; assert!(get_latest(args).await.is_ok()); diff --git a/src/api/gitlab.rs b/src/api/gitlab.rs index 1d1b34f..d173c15 100644 --- a/src/api/gitlab.rs +++ b/src/api/gitlab.rs @@ -1,5 +1,6 @@ -use crate::api; -use reqwest::header::HeaderValue; +use crate::{api, error}; +use reqwest::{header::HeaderValue, Response}; +use serde_json::Value; #[derive(serde::Deserialize)] struct GitLabResponse { @@ -14,44 +15,72 @@ pub fn get_latest(args: api::ApiArgs) -> api::ReleaseFuture { } else { "gitlab.com" }; - let url = format!( - "https://{}/api/v4/projects/{}/releases/permalink/latest", + let repo_url = format!( + "https://{}/api/v4/projects/{}", host, args.args[0].replace("/", "%2F") ); - let mut headers = api::setup_headers(); - if !args.api_key.is_empty() { - headers.insert( - "PRIVATE-TOKEN", - HeaderValue::from_str(&args.api_key).unwrap(), - ); - }; - let client = args.request_client; - let result = client.get(url).headers(headers).send().await?; - api::match_statuscode(&result.status(), args.package)?; + if args.use_max_tag.is_some_and(|x| x) { + let url = format!("{}/repository/tags", repo_url); + let result = request(url, &args).await?; + let json: Value = result.json().await?; + + let max_tag = json + .get(0) + .unwrap() + .get("name") + .unwrap() + .to_string() + .replace("\"", ""); - let json: GitLabResponse = result.json().await?; + Ok(api::Release { + name: max_tag.clone(), + tag: Some(max_tag.clone()), + url: format!("https://{}/{}/-/tags/{}", host, args.args[0], max_tag), + }) + } else { + let url = format!("{}/releases/permalink/latest", repo_url); + let result = request(url, &args).await?; + let json: GitLabResponse = result.json().await?; - Ok(api::Release { - name: json.tag_name.clone(), - tag: Some(json.tag_name), - url: format!("https://{}{}", host, json.tag_path), - }) + Ok(api::Release { + name: json.tag_name.clone(), + tag: Some(json.tag_name), + url: format!("https://{}{}", host, json.tag_path), + }) + } }) } +async fn request(url: String, args: &api::ApiArgs) -> error::Result { + let mut headers = api::setup_headers(); + if !args.api_key.is_empty() { + headers.insert( + "PRIVATE-TOKEN", + HeaderValue::from_str(&args.api_key).unwrap(), + ); + }; + let client = &args.request_client; + + let result = client.get(url).headers(headers).send().await?; + api::match_statuscode(&result.status(), args.package.clone())?; + + Ok(result) +} + #[tokio::test] async fn request_test() { let package = "mkinitcpio".to_string(); let args = api::ApiArgs { + request_client: reqwest::Client::new(), package: package.clone(), + use_max_tag: None, args: vec![ format!("archlinux/{0}/{0}", package), "gitlab.archlinux.org".to_string(), ], api_key: String::new(), - request_client: reqwest::Client::new(), }; assert!(get_latest(args).await.is_ok()); diff --git a/src/api/mod.rs b/src/api/mod.rs index cbdeb92..7d1f29a 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -17,6 +17,7 @@ pub struct Api { pub struct ApiArgs { pub request_client: reqwest::Client, pub package: String, + pub use_max_tag: Option, pub args: Vec, pub api_key: String, // empty String if none } diff --git a/src/config.rs b/src/config.rs index 4d434fe..a894bb3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -55,6 +55,8 @@ pub struct Package { #[serde(skip_serializing_if = "is_empty_string")] gitlab: String, + #[serde(default)] + pub use_max_tag: Option, #[serde(default)] #[serde(skip_serializing_if = "is_empty_string")] pub prefix: String, diff --git a/src/lib.rs b/src/lib.rs index 8e42427..7400dd1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,6 +72,7 @@ pub async fn run_source( let args = api::ApiArgs { request_client: client, package: package.0, + use_max_tag: package.1.use_max_tag, args: api_args, api_key, }; From 8d7e3413e258ac1b1a38256de10f02d8f078d68d Mon Sep 17 00:00:00 2001 From: Adam Perkowski Date: Wed, 27 Nov 2024 03:18:37 +0100 Subject: [PATCH 11/14] fix(ui): `sync` errors displayed twice when no `--no-fail` Signed-off-by: Adam Perkowski --- src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 053e4d8..d09a869 100644 --- a/src/main.rs +++ b/src/main.rs @@ -208,9 +208,10 @@ async fn sync(core: Core, no_fail: bool) -> error::Result<()> { } } Err(e) => { - pretty_error(&e); if !no_fail { return Err(e); + } else { + pretty_error(&e); } } }; From 1aeacbc99edd0409d01798ffb30460d197318190 Mon Sep 17 00:00:00 2001 From: Adam Perkowski Date: Wed, 27 Nov 2024 03:46:43 +0100 Subject: [PATCH 12/14] chore(deps): update external deps Signed-off-by: Adam Perkowski --- Cargo.lock | 76 +++++++++++++++++++++++++++--------------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 799090d..29f60d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,9 +405,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "h2" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" dependencies = [ "atomic-waker", "bytes", @@ -424,9 +424,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" @@ -482,9 +482,9 @@ checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "hyper" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", @@ -715,9 +715,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" @@ -736,9 +736,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.164" +version = "0.2.166" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" +checksum = "c2ccc108bbc0b1331bd061864e7cd823c0cab660bbe6970e66e2c0614decde36" [[package]] name = "linux-raw-sys" @@ -748,9 +748,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" @@ -942,9 +942,9 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -1032,9 +1032,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.40" +version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ "bitflags", "errno", @@ -1090,9 +1090,9 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] @@ -1148,9 +1148,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -1239,9 +1239,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.87" +version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ "proc-macro2", "quote", @@ -1250,9 +1250,9 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ "futures-core", ] @@ -1450,9 +1450,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] @@ -1465,9 +1465,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "untrusted" @@ -1477,9 +1477,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -1803,9 +1803,9 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -1815,9 +1815,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", @@ -1827,18 +1827,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", From 712bceae2626838af664df10dd967cb4a2819ab8 Mon Sep 17 00:00:00 2001 From: Adam Perkowski Date: Wed, 27 Nov 2024 16:37:17 +0100 Subject: [PATCH 13/14] refact(features): remove `http` Signed-off-by: Adam Perkowski --- Cargo.toml | 11 +++++------ src/api/mod.rs | 2 -- src/error.rs | 1 - 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a09011f..d6eab03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,17 +20,16 @@ include = [ ] [features] -default = ["http", "aur", "github", "gitlab"] -http = ["reqwest"] -aur = ["http"] -github = ["http"] -gitlab = ["http"] +default = ["aur", "github", "gitlab"] +aur = [] +github = [] +gitlab = [] [dependencies] clap = { version = "4.5.21", features = ["derive", "color", "error-context", "help", "std", "usage"], default-features = false } colored = "2.1.0" futures = "0.3.31" -reqwest = { version = "0.12.9", features = ["__tls", "charset", "default-tls", "h2", "http2", "json"], default-features = false, optional = true } +reqwest = { version = "0.12.9", features = ["__tls", "charset", "default-tls", "h2", "http2", "json"], default-features = false } serde = { version = "1.0.215", features = ["derive"], default-features = false } serde_json = "1.0.132" thiserror = "2.0.3" diff --git a/src/api/mod.rs b/src/api/mod.rs index 7d1f29a..c904ccd 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -34,7 +34,6 @@ pub struct Release { type ReleaseFuture = std::pin::Pin> + Send>>; -#[cfg(feature = "http")] fn setup_headers() -> reqwest::header::HeaderMap { use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT}; @@ -44,7 +43,6 @@ fn setup_headers() -> reqwest::header::HeaderMap { headers } -#[cfg(feature = "http")] fn match_statuscode(status: &reqwest::StatusCode, package: String) -> crate::error::Result<()> { use crate::error; use reqwest::StatusCode; diff --git a/src/error.rs b/src/error.rs index 95a3166..50d6762 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,7 +14,6 @@ newver = \"newver.json\""; #[derive(Debug, ThisError)] pub enum Error { - #[cfg(feature = "http")] #[error("request error: {0}")] RequestError(#[from] reqwest::Error), From ca96da8381da62cea1b01fd1f1d0363b7e5d1f9b Mon Sep 17 00:00:00 2001 From: Adam Perkowski Date: Wed, 27 Nov 2024 17:02:56 +0100 Subject: [PATCH 14/14] test: `Package` default(), new() & tests Signed-off-by: Adam Perkowski --- src/config.rs | 64 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/src/config.rs b/src/config.rs index a894bb3..0baff72 100644 --- a/src/config.rs +++ b/src/config.rs @@ -62,8 +62,50 @@ pub struct Package { pub prefix: String, } -// TODO: Package defaults & tests impl Package { + pub fn new( + source: String, + target: String, + use_max_tag: bool, + prefix: String, + ) -> error::Result { + let mut package = Package::default(); + + match source.as_ref() { + "aur" => { + package.aur = target; + Ok(()) + } + "github" => { + package.github = target; + Ok(()) + } + "gitlab" => { + package.gitlab = target; + Ok(()) + } + _ => Err(error::Error::SourceNotFound(source.clone())), + }?; + + package.source = source; + package.use_max_tag = Some(use_max_tag); + package.prefix = prefix; + + Ok(package) + } + + fn default() -> Self { + Package { + source: String::new(), + host: String::new(), + aur: String::new(), + github: String::new(), + gitlab: String::new(), + use_max_tag: None, + prefix: String::new(), + } + } + /// global function to get various API-specific agrs for a package /// /// # example @@ -156,8 +198,24 @@ mod tests { async fn loading() { let config = load(None).await.unwrap(); - // TODO: here, ref L47 - assert_eq!(config.1, PathBuf::from("nvrs.toml")); } + + #[tokio::test] + async fn manual_package() { + assert!(Package::new( + "non_existing_source".to_string(), + "non_existing".to_string(), + false, + String::new() + ) + .is_err()); + assert!(Package::new( + "github".to_string(), + "orhun/git-cliff".to_string(), + false, + "v".to_string() + ) + .is_ok()); + } }