16 Commits

Author SHA1 Message Date
17bde93259 Update changelog for 2.2.0-1 release 2025-07-04 10:11:04 -05:00
cdb312fe61 Add the new dependencies to debian/control
Debian's package machinery also needs to know about these things, so
throw them into the control file.
2025-07-04 10:07:47 -05:00
cfaa0ceb3f Lift the empty-body string outside the let-if
Patch the string reference lifetime issue, and rediff the other patch
while we're at it.
2025-07-04 09:57:16 -05:00
c0a0181074 Merge tag 'v2.2.0' into deb/bookworm 2025-07-04 09:43:25 -05:00
119831481e Bump to v2.2.0
All checks were successful
/ Compile and upload a release build (release) Successful in 44s
2025-07-03 18:13:09 -05:00
7246c7afb6 Oops, missed one 2025-07-03 18:05:18 -05:00
84eaaa1dbd Autoformat 2025-07-03 18:03:33 -05:00
c9dda5760c Prefix unused variables to quiet the linter 2025-07-03 17:56:07 -05:00
336f1453b9 Address most of the cargo-clippy lints 2025-07-03 17:56:07 -05:00
f068e8233e Release.colorized(), not std::fmt::Display
I don't know for sure if the string-ified version of a Release struct is
being printed to the terminal. As such, I don't know if the user wants,
does not want, or has mixed intentions for the stringification of this
thing.

No Display impl, instead just a `colorized()` method.
2025-07-03 17:47:50 -05:00
d4ef21e243 Change to free-fn intersperse for stdlib compat
Itertools warns that the standard library may be stabilizing the
intersperse method soon and recommends using this function instead.
2025-07-02 22:44:06 -05:00
d94c350cde Galaxy-brained newline intersperse function
Itertools already has an intersperse method for me. Why would I build my
own when I can do this? There's even a `fold()` over the units that come
out of the print routine.
2025-07-02 22:29:07 -05:00
8120cb0489 Remove trailing newline in Release item printout 2025-07-02 22:08:45 -05:00
b82cfdb822 Colorize the output! 2025-07-02 22:06:36 -05:00
ea046c929f Print releases in reverse order for easier reading
The result list has the newest item first, but I want to print them the
other way around. This way the newest (and presumably most interesting)
release is always the visible item, regardless of how many others have
printed and scrolled off screen.
2025-07-02 21:42:41 -05:00
135acf09b7 Basic impl Display for the Release struct
I'm not certain what info I want to present when listing the Releases.

The idea is that the release version is the most important, and that it
matches the git-tag associated with the release. I'll print that first.

Next, the name of the release followed by the body text. The list of
releases will become quite large for some projects, and the body text
may include a changelog. Both of these will cause the output to become
quite large. I will need to create a size limiter, but I'm ignoring that
for now.

Who created the release and when may be useful when searching for a
release, so I've included that as the final section.
2025-07-02 12:56:17 -05:00
11 changed files with 130 additions and 40 deletions

View File

@@ -1,10 +1,12 @@
[package] [package]
name = "gt-tool" name = "gt-tool"
version = "1.0.0" version = "2.2.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
clap = { version = "4.0.7", features = ["derive", "env"] } clap = { version = "4.0.7", features = ["derive", "env"] }
colored = "2.0.0"
itertools = "0.10.0"
reqwest = { version = "0.11.13", features = ["json", "stream", "multipart"] } reqwest = { version = "0.11.13", features = ["json", "stream", "multipart"] }
serde = { version = "1.0.152", features = ["derive"] } serde = { version = "1.0.152", features = ["derive"] }
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] }

19
debian/changelog vendored
View File

@@ -1,3 +1,22 @@
gt-tool (2.2.0-1) unstable; urgency=medium
* Basic impl Display for the Release struct
* Print releases in reverse order for easier reading
* Colorize the output!
* Remove trailing newline in Release item printout
* Galaxy-brained newline intersperse function
* Change to free-fn intersperse for stdlib compat
* `Release.colorized()`, not std::fmt::Display
* Address most of the cargo-clippy lints
* Prefix unused variables to quiet the linter
* Autoformat
* Oops, missed one
* Bump to v2.2.0
* Lift the empty-body string outside the let-if
* Add the new dependencies to debian/control
-- Robert Garrett <robertgarrett404@gmail.com> Fri, 04 Jul 2025 10:10:54 -0500
gt-tool (2.1.0-1) unstable; urgency=medium gt-tool (2.1.0-1) unstable; urgency=medium
* Fix: incorrect field names for `Attachment` * Fix: incorrect field names for `Attachment`

2
debian/control vendored
View File

@@ -7,6 +7,8 @@ Build-Depends:
debhelper-compat (= 13), debhelper-compat (= 13),
dh-cargo, dh-cargo,
librust-clap-dev, librust-clap-dev,
librust-colored-dev,
librust-itertools-dev,
librust-reqwest-dev, librust-reqwest-dev,
librust-tokio-dev, librust-tokio-dev,
librust-serde-dev, librust-serde-dev,

View File

@@ -2,20 +2,20 @@ From: Robert Garrett <robertgarrett404@gmail.com>
Date: Sun, 1 Jun 2025 17:59:20 -0500 Date: Sun, 1 Jun 2025 17:59:20 -0500
Subject: Rust edition downgrade to 2021 Subject: Rust edition downgrade to 2021
Debian Bookworm uses Rust 1.64 which only supports up to the 2021 Debian Bookworm uses Rust 1.63 which only supports up to the 2021
edition. edition.
--- ---
Cargo.toml | 2 +- Cargo.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-) 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Cargo.toml b/Cargo.toml diff --git a/Cargo.toml b/Cargo.toml
index febccc4..cf52754 100644 index 4fd569c..8b67a52 100644
--- a/Cargo.toml --- a/Cargo.toml
+++ b/Cargo.toml +++ b/Cargo.toml
@@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
[package] [package]
name = "gt-tool" name = "gt-tool"
version = "1.0.0" version = "2.2.0"
-edition = "2024" -edition = "2024"
+edition = "2021" +edition = "2021"

View File

@@ -0,0 +1,39 @@
From: Robert Garrett <robertgarrett404@gmail.com>
Date: Fri, 4 Jul 2025 09:36:52 -0500
Subject: Lift the empty-body string outside the let-if
The if-else block that selects between the body of the Release or a
placeholder is returning references to variables that only exist
*inside* the body of the if-else blocks. Newer Rust versions seem to
understand the intent and do The Right Thing anyway (or they have some
other rule for how if-else block scopes work).
Manually lifting the variable to an outer scope resolves the problem.
---
src/structs/release.rs | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/structs/release.rs b/src/structs/release.rs
index 9ed537e..3c4a434 100644
--- a/src/structs/release.rs
+++ b/src/structs/release.rs
@@ -1,3 +1,5 @@
+use std::io::empty;
+
use colored::Colorize;
use serde::{Deserialize, Serialize};
@@ -27,10 +29,11 @@ impl Release {
let published = "Published:".bright_green();
let created = "Created:".green().dimmed();
let author = "Author:".blue();
+ let empty_body = String::from("(empty body)").dimmed();
let body = if !self.body.is_empty() {
- &self.body.white()
+ self.body.white()
} else {
- &String::from("(empty body)").dimmed()
+ empty_body
};
format!(

View File

@@ -1 +1,2 @@
0001-Rust-edition-downgrade-to-2021.patch 0001-Rust-edition-downgrade-to-2021.patch
0002-Lift-the-empty-body-string-outside-the-let-if.patch

View File

@@ -1,12 +1,9 @@
use crate::{ use crate::{
Result, Result,
structs::{ structs::release::{CreateReleaseOption, Release},
release::{CreateReleaseOption, Release},
},
}; };
pub fn get_release(id: u64) -> Result<Release> { pub fn get_release(_id: u64) -> Result<Release> {
todo!(); todo!();
} }
pub fn get_latest_release() -> Result<Release> { pub fn get_latest_release() -> Result<Release> {
@@ -20,7 +17,7 @@ pub async fn list_releases(
) -> Result<Vec<Release>> { ) -> Result<Vec<Release>> {
let request_url = format!("{gitea_url}/api/v1/repos/{repo}/releases/"); let request_url = format!("{gitea_url}/api/v1/repos/{repo}/releases/");
let req = client.get(request_url).send().await; let req = client.get(request_url).send().await;
let response = req.map_err(|reqwest_err| crate::Error::WrappedReqwestErr(reqwest_err))?; let response = req.map_err(crate::Error::WrappedReqwestErr)?;
if response.status().is_success() { if response.status().is_success() {
let release_list = response let release_list = response
.json::<Vec<Release>>() .json::<Vec<Release>>()
@@ -50,22 +47,22 @@ pub async fn create_release(
.json(&submission) .json(&submission)
.send() .send()
.await .await
.map_err(|e| crate::Error::from(e))?; .map_err(crate::Error::from)?;
if response.status().is_success() { if response.status().is_success() {
let new_release = response let new_release = response
.json::<Release>() .json::<Release>()
.await .await
.map_err(|e| crate::Error::from(e))?; .map_err(crate::Error::from)?;
return Ok(new_release); return Ok(new_release);
} else if response.status().is_client_error() { } else if response.status().is_client_error() {
let mesg = crate::decode_client_error(response).await?; let mesg = crate::decode_client_error(response).await?;
return Err(crate::Error::ApiErrorMessage(mesg)) return Err(crate::Error::ApiErrorMessage(mesg));
} }
panic!("Reached end of create_release without matching a return path"); panic!("Reached end of create_release without matching a return path");
} }
pub fn edit_release(id: u64) -> Result<Release> { pub fn edit_release(_id: u64) -> Result<Release> {
todo!(); todo!();
} }
pub fn delete_release(id: u64) -> Result<()> { pub fn delete_release(_id: u64) -> Result<()> {
todo!(); todo!();
} }

View File

@@ -22,8 +22,10 @@ pub async fn create_release_attachment(
Ok(false) => return Err(crate::Error::NoSuchFile), Ok(false) => return Err(crate::Error::NoSuchFile),
Err(e) => { Err(e) => {
eprintln!("Uh oh! The file-exists check couldn't be done: {e}"); eprintln!("Uh oh! The file-exists check couldn't be done: {e}");
panic!("TODO: Deal with scenario where the file's existence cannot be checked (e.g.: no permission)"); panic!(
}, "TODO: Deal with scenario where the file's existence cannot be checked (e.g.: no permission)"
);
}
} }
println!("Uploading file {}", &file); println!("Uploading file {}", &file);
@@ -44,7 +46,7 @@ pub async fn create_release_attachment(
let attachment_desc = response let attachment_desc = response
.json::<Attachment>() .json::<Attachment>()
.await .await
.map_err(|e| crate::Error::from(e))?; .map_err(crate::Error::from)?;
return Ok(attachment_desc); return Ok(attachment_desc);
} else if response.status().is_client_error() { } else if response.status().is_client_error() {
let mesg = crate::decode_client_error(response).await?; let mesg = crate::decode_client_error(response).await?;

View File

@@ -14,9 +14,7 @@ pub (crate) async fn decode_client_error(response: reqwest::Response) -> Result<
response response
.json::<ApiError>() .json::<ApiError>()
.await .await
.map_err(|reqwest_err| { .map_err(crate::Error::WrappedReqwestErr)
crate::Error::WrappedReqwestErr(reqwest_err)
})
} }
#[derive(Debug)] #[derive(Debug)]

View File

@@ -1,4 +1,3 @@
use std::path; use std::path;
use gt_tool::cli::Args; use gt_tool::cli::Args;
@@ -22,10 +21,7 @@ async fn main() -> Result<(), gt_tool::Error> {
headers.append("Authorization", token.parse().unwrap()); headers.append("Authorization", token.parse().unwrap());
} }
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.user_agent(format!( .user_agent(format!("gt-tools-agent-{}", env!("CARGO_PKG_VERSION")))
"gt-tools-agent-{}",
env!("CARGO_PKG_VERSION")
))
.default_headers(headers) .default_headers(headers)
.build()?; .build()?;
@@ -33,9 +29,15 @@ async fn main() -> Result<(), gt_tool::Error> {
gt_tool::cli::Commands::ListReleases => { gt_tool::cli::Commands::ListReleases => {
let releases = let releases =
gt_tool::api::release::list_releases(&client, &args.gitea_url, &args.repo).await?; gt_tool::api::release::list_releases(&client, &args.gitea_url, &args.repo).await?;
for release in releases { // Print in reverse order so the newest items are closest to the
println!("{:?}", release); // user's command prompt. Otherwise the newest item scrolls off the
} // screen and can't be seen.
itertools::Itertools::intersperse(
releases.iter().rev().map(|release| release.colorized()),
String::from(""),
)
.map(|release| println!("{}", release))
.fold((), |_, _| ());
} }
gt_tool::cli::Commands::CreateRelease { gt_tool::cli::Commands::CreateRelease {
name, name,
@@ -52,12 +54,7 @@ async fn main() -> Result<(), gt_tool::Error> {
tag_name, tag_name,
target_commitish, target_commitish,
}; };
gt_tool::api::release::create_release( gt_tool::api::release::create_release(&client, &args.gitea_url, &args.repo, submission)
&client,
&args.gitea_url,
&args.repo,
submission,
)
.await?; .await?;
} }
gt_tool::cli::Commands::UploadRelease { gt_tool::cli::Commands::UploadRelease {
@@ -88,8 +85,10 @@ async fn main() -> Result<(), gt_tool::Error> {
Ok(false) => return Err(gt_tool::Error::NoSuchFile), Ok(false) => return Err(gt_tool::Error::NoSuchFile),
Err(e) => { Err(e) => {
eprintln!("Uh oh! The file-exists check couldn't be done: {e}"); eprintln!("Uh oh! The file-exists check couldn't be done: {e}");
panic!("TODO: Deal with scenario where the file's existence cannot be checked (e.g.: no permission)"); panic!(
}, "TODO: Deal with scenario where the file's existence cannot be checked (e.g.: no permission)"
);
}
} }
} }
for file in files { for file in files {
@@ -143,5 +142,5 @@ fn match_release_by_tag(tag: &String, releases: Vec<Release>) -> Option<Release>
} }
} }
} }
return release; release
} }

View File

@@ -1,3 +1,4 @@
use colored::Colorize;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
@@ -19,6 +20,36 @@ pub struct Release {
author: Author, author: Author,
} }
impl Release {
pub fn colorized(&self) -> String {
let tag = "Tag:".green().bold();
let name = "Name:".green();
let published = "Published:".bright_green();
let created = "Created:".green().dimmed();
let author = "Author:".blue();
let body = if !self.body.is_empty() {
&self.body.white()
} else {
&String::from("(empty body)").dimmed()
};
format!(
"{tag} {}
{name} {}
{}
{published} {} ({created} {})
{author} {} ({})",
self.tag_name.bold(),
self.name,
body,
self.published_at,
self.created_at.dimmed(),
self.author.login,
self.author.email,
)
}
}
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Author { pub struct Author {
id: usize, id: usize,