17 Commits

Author SHA1 Message Date
247c06dd9e Rename the config-string-reading function 2025-07-17 13:32:55 -05:00
cb314a8b4c Assert empty conf str is an error, TODO: semantics
The empty configuration string is some kind of an error, but I'm not
sure where and how to handle it. It should be treated as a soft error,
where I fall back to some hardcoded defaults.

There's a logic hole at the moment: The error I'm actually getting right
now is "NoSuchTable" because the "[all]" table doesn't exist. For a
totally empty config file, the above response should be used. But what
about a non-empty conf file? Is a missing "[all]" valid or not? For now,
assert the loader returns *an* error and leave behind a TODO for later.
2025-07-17 13:29:25 -05:00
277f638c60 Add a builder-pattern proj-path setter, for flavor
I like being able to chain methods instead of using a temporary variable
in between, so I've made one single function like I'm doing the builder
pattern.

But not really because there's nothing to build or finalize and such.
2025-07-17 12:13:14 -05:00
626973d2bc Extract PartCfg readers to a try_from impl
Don't repeat yourself. These property reading routines are actually
methods on the PartialConfig struct, so make them *actually* methods.

Because the table doesn't know it's own name, the path-specific config
needs to be updated with that external knowledge.
2025-07-17 12:08:35 -05:00
28539f54cc Use the get_table util to extract "[all]" table
I built the function for this purpose and then forgot to use it. I
remembered after doing the per-project bit, so here's the refactor.
2025-07-17 11:49:44 -05:00
5ce20adf2e Create-and-assign struct to whole.all
Minor refactor to make the "[all]" table read look like the per-project
table reads.
2025-07-17 11:47:18 -05:00
fc1e20185e Finish fn lconf(). Project-specific vals load
Loop over the keys (ignore the "all" one) and repeat the same property
extraction process.
2025-07-17 11:41:28 -05:00
15593204e0 Put the per-project test expects in for lconf()
Put all those per-project configs into the unit test. They're in the
input string, so we should expect to see them in the output struct.
2025-07-17 10:28:17 -05:00
330985940f Prototype load-config function
The `lconf()` function will eventually load the whole file, but for now
it reads in only the "[all]" table.

That "[all]" table will be used as the global fallback when per-project
settings are left unspecified.

The unit test "passes" but only because I've discarded those per-project
configs from the expected result. This is just so I can see clearly that
the all-table is loading properly.
2025-07-17 10:25:26 -05:00
213e0b4f4a Add partial- and whole- config structs 2025-07-17 10:23:00 -05:00
d27bea2c43 Util to get sometimes-empty config property
The get_property function needs to say that there is no property so that
the caller can respond appropriately. I'm going to need to frequently
respond to the "no such property" path by treating it as *not* an error.

If the config file doesn't specify a property, that's not an error, it's
just not specified and the default should be used instead. This util fn
makes that a bit more ergonomic.
2025-07-17 09:59:52 -05:00
30d8bcc6de Util fn's can use anything that impl's ToString
I don't want to remember to construct a `String` every single time I
want to call this function with a string literal. So I won't.

Make the functions generic over anything that implements the ToString
trait.
2025-07-17 09:57:49 -05:00
b2b9c8b9d9 Add a get-table util function 2025-07-16 19:16:43 -05:00
f6bab75644 Add property-get utility function
This function will look for a given property in a given table. It gives
back either the property, or an explanation of why it could not be
retrieved.
2025-07-14 21:43:08 -05:00
075a2ee921 Scaffold the new config module 2025-07-06 17:28:11 -05:00
8eacb510a2 Add a Cargo.toml & Git tag version comparison
I released the first couple versions without updating the value in
Cargo.toml. This will check for that happening again and abort the
build.
2025-07-06 12:54:52 -05:00
a23bdf3e34 Make the README title singular
It's not the "gt-tools" anymore, so maybe the README should match.
2025-07-05 17:21:59 -05:00
14 changed files with 267 additions and 221 deletions

View File

@@ -8,6 +8,11 @@ jobs:
name: Compile and upload a release build
steps:
- uses: actions/checkout@v4
- name: Get Cargo version
run: echo "cargo_version=v$(grep "^version" Cargo.toml | cut -d \" -f2)" >> $GITHUB_ENV
- name: Abort if Cargo.toml & Git Tag versions don't match
if: ${{ env.cargo_version != github.ref_name }}
run: exit 1
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
- name: Build binary crate

View File

@@ -10,6 +10,7 @@ itertools = "0.10.0"
reqwest = { version = "0.11.13", features = ["json", "stream", "multipart"] }
serde = { version = "1.0.152", features = ["derive"] }
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] }
toml = "0.5"
# Packages available in Debian (Sid)
# clap = "4.5.23"

View File

@@ -1,4 +1,4 @@
# gt-tools
# gt-tool
CLI tools for interacting with the Gitea API. Use interactively to talk to your Gitea instance, or automatically via a CI/CD pipeline.

48
debian/changelog vendored
View File

@@ -1,48 +0,0 @@
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
* Fix: incorrect field names for `Attachment`
-- Robert Garrett <robertgarrett404@gmail.com> Thu, 12 Jun 2025 17:51:12 -0500
gt-tool (2.0.0-1) unstable; urgency=medium
* Interrogate list_releases result more closely
* Interrogate create_release result more closely
* Drop unused imports
* "Fix" the test case
* Interrogate create_release_attachment result
* Fold client-error-decode into a util function
* Add `Attachment` struct, new iface for create-rel
* Update main.rs to use new attachment iface
* Delete the unit tests
* ... and the unit testing notes in README.md
* Drop unused import in api/release.rs
* Use pre Rust 1.81 compatible file-exists test
* Rediff patches
-- Robert Garrett <robertgarrett404@gmail.com> Thu, 12 Jun 2025 16:28:18 -0500
gt-tool (1.0.0-1) unstable; urgency=low
* Experimental release.
-- Robert Garrett <robertgarrett404@gmail.com> Sun, 1 Jun 2025 16:05:00 -0500

32
debian/control vendored
View File

@@ -1,32 +0,0 @@
Source: gt-tool
Maintainer: Robert Garrett <robertgarrett404@gmail.com>
Section: misc
Priority: optional
Standards-Version: 4.6.2
Build-Depends:
debhelper-compat (= 13),
dh-cargo,
librust-clap-dev,
librust-colored-dev,
librust-itertools-dev,
librust-reqwest-dev,
librust-tokio-dev,
librust-serde-dev,
Homepage: https://git.gelvin.dev/robert/gt-tool
Vcs-Git: https://git.gelvin.dev/robert/gt-tool
Vcs-Browser: https://git.gelvin.dev/robert/gt-tool
Rules-Requires-Root: no
Package: gt-tool
Architecture: any
Depends:
${misc:Depends},
${shlibs:Depends},
Description: CLI tools for interacting with the Gitea API.
Use interactively to talk to your Gitea instance, or automatically via a CI/CD
pipeline. Currently supports:
.
- showing the Releases for a project
- creating a new Release for a project
- attaching files to a release

43
debian/copyright vendored
View File

@@ -1,43 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: gt-tools
Upstream-Contact: Robert Garrett <robertgarrett404@gmail.com>
Source: https://source.mnt.re/reform/mnt-reform-setup-wizard
Files: *
Copyright: 2025 Robert Garrett <robertgarrett404@gmail.com>
License: GPL-3+
Files: debian/*
Copyright: 2025 Robert Garrett <robertgarrett404@gmail.com>
License: GPL-3+
Files: debian/rules
Copyright:
Johannes Schauer Marin Rodrigues <josch@debian.org>
2025 Robert Garrett <robertgarrett404@gmail.com>
License: GPL-3+
Comment:
The debian/rules file is liften directly from the tuigreet package. It was
linked in the Debian Rust Team Book as a pretty simple example package. The
only change I've made is to remove the documentation generation target.
.
https://salsa.debian.org/debian/tuigreet/-/blob/master/debian/rules?ref_type=heads
License: GPL-3+
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
.
It is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
License for more details.
.
You should have received a copy of the GNU General Public License
along with it. If not, see <http://www.gnu.org/licenses/>.
.
On Debian systems, the full text of the GNU General Public License version 3
can be found in the file /usr/share/common-licenses/GPL-3.

6
debian/gbp.conf vendored
View File

@@ -1,6 +0,0 @@
[DEFAULT]
compression = xz
compression-level = 9
upstream-tag = v%(version)s
debian-branch = deb/bookworm

View File

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

View File

@@ -1,39 +0,0 @@
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,2 +0,0 @@
0001-Rust-edition-downgrade-to-2021.patch
0002-Lift-the-empty-body-string-outside-the-let-if.patch

26
debian/rules vendored
View File

@@ -1,26 +0,0 @@
#!/usr/bin/make -f
export DEB_BUILD_MAINT_OPTIONS = hardening=+all
DPKG_EXPORT_BUILDFLAGS = 1
include /usr/share/dpkg/default.mk
include /usr/share/rustc/architecture.mk
export DEB_HOST_RUST_TYPE
export PATH:=/usr/share/cargo/bin:$(PATH)
export CARGO=/usr/share/cargo/bin/cargo
export CARGO_HOME=$(CURDIR)/debian/cargo_home
export CARGO_REGISTRY=$(CURDIR)/debian/cargo_registry
export DEB_CARGO_CRATE=$(DEB_SOURCE)_$(DEB_VERSION_UPSTREAM)
%:
dh $@ --buildsystem=cargo
execute_after_dh_auto_clean:
$(CARGO) clean
rm -rf $(CARGO_HOME)
rm -rf $(CARGO_REGISTRY)
rm -f debian/cargo-checksum.json
execute_before_dh_auto_configure:
$(CARGO) prepare-debian $(CARGO_REGISTRY) --link-from-system
rm -f Cargo.lock
touch debian/cargo-checksum.json

View File

@@ -1 +0,0 @@
3.0 (quilt)

259
src/config.rs Normal file
View File

@@ -0,0 +1,259 @@
use toml::{Value, value::Table};
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub enum Error {
BadFormat,
NoSuchProperty,
NoSuchTable,
TomlWrap(toml::de::Error),
}
impl From<toml::de::Error> for Error {
fn from(value: toml::de::Error) -> Self {
Error::TomlWrap(value)
}
}
impl core::fmt::Display for Error{
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// FIXME: Print a nice output, don't just reuse the Debug impl
write!(fmt, "{self:?}")
}
}
impl std::error::Error for Error {}
#[derive(Debug, Default)]
#[cfg_attr(test, derive(PartialEq))]
struct PartialConfig {
project_path: Option<String>,
gitea_url: Option<String>,
owner: Option<String>,
repo: Option<String>,
token: Option<String>,
}
impl PartialConfig {
// One lonely builder-pattern function to set the project path.
// This is so I can do continuation style calls instead of a bunch of
// successive `let conf = ...` temporaries.
fn project_path(self, path: impl ToString) -> Self{
PartialConfig {
project_path: Some(path.to_string()),
..self
}
}
}
impl TryFrom<&Table> for PartialConfig {
type Error = crate::config::Error;
fn try_from(value: &Table) -> Result<Self> {
Ok(Self {
// can't get table name because that key is gone by this point.
project_path: None,
gitea_url: get_maybe_property(&value, "gitea_url")?.cloned(),
owner: get_maybe_property(&value, "owner")?.cloned(),
repo: get_maybe_property(&value, "repo")?.cloned(),
token: get_maybe_property(&value, "token")?.cloned(),
})
}
}
#[derive(Debug, Default)]
#[cfg_attr(test, derive(PartialEq))]
struct WholeFile {
all: PartialConfig,
project_overrides: Vec<PartialConfig>,
}
fn read_conf_str(text: &str) -> Result<WholeFile> {
let mut whole = WholeFile::default();
let toml_val = text.parse::<Value>()?;
// The config file is one big table. If the string we decoded is
// some other toml::Value variant, it's not correct.
// Try getting it as a table, return Err(BadFormat) otherwise.
let cfg_table = toml_val.as_table().ok_or(Error::BadFormat)?;
// Get the global config out of the file
let table_all = get_table(cfg_table, "all")?;
whole.all = PartialConfig::try_from(table_all)?;
// Loop over the per-project configs, if any.
let per_project_keys = cfg_table
.keys()
.filter(|s| { // Discard the "[all]" table
*s != "all"
});
for path in per_project_keys {
let tab = get_table(cfg_table, path)?;
let part_cfg = PartialConfig::try_from(tab)?
.project_path(path.clone());
whole.project_overrides.push(part_cfg);
}
println!(" ->> lconf - keys {:?}", cfg_table.keys().collect::<Vec<&String>>());
Ok(whole)
}
/// The outer value must be a Table so we can get the sub-table from it.
fn get_table<'outer>(outer: &'outer Table, table_name: impl ToString) -> Result<&'outer Table> {
Ok(outer
.get(&table_name.to_string())
.ok_or(Error::NoSuchTable)?
.as_table()
.ok_or(Error::BadFormat)?)
}
/// Similar to `get_property()` but maps the "Error::NoSuchProperty" result to
/// Option::None. Some properties aren't specified, and that's okay... sometimes.
fn get_maybe_property<'outer> (outer: &'outer Table, property: impl ToString) -> Result<Option<&'outer String>> {
let maybe_prop = get_property(outer, property);
match maybe_prop {
Ok(value) => Ok(Some(value)),
Err(e) => {
if let Error::NoSuchProperty = e {
return Ok(None);
} else {
return Err(e);
}
}
}
}
/// The config properties are individual strings. This gets the named property,
/// or an error explaining why it couldn't be fetched.
fn get_property<'outer>(outer: &'outer Table, property: impl ToString) -> Result<&'outer String> {
let maybe_prop = outer.get(&property.to_string()).ok_or(Error::NoSuchProperty)?;
if let Value::String(text) = maybe_prop {
Ok(text)
} else {
Err(Error::BadFormat)
}
}
#[cfg(test)]
mod tests {
use toml::map::Map;
use super::*;
#[test]
fn read_single_prop() -> Result<()> {
let fx_input_str = "owner = \"dingus\"";
let fx_value = fx_input_str.parse::<Value>()?;
let fx_value = fx_value.as_table().ok_or(Error::NoSuchTable)?;
let expected = "dingus";
let res = get_property(&fx_value, String::from("owner"))?;
assert_eq!(res, expected);
Ok(())
}
// The property is given the value of empty-string `""`
#[test]
fn read_single_prop_empty_quotes() -> Result<()> {
let fx_input_str = "owner = \"\"";
let fx_value = fx_input_str.parse::<Value>()?;
let fx_value = fx_value.as_table().ok_or(Error::NoSuchTable)?;
let expected = "";
let res = get_property(&fx_value, String::from("owner"))?;
assert_eq!(res, expected);
Ok(())
}
#[test]
fn read_table() -> Result<()> {
let fx_input_str = "[tab]\nwith_a_garbage = \"value\"";
let fx_value = fx_input_str.parse::<Value>()?;
let fx_value = fx_value.as_table().ok_or(Error::BadFormat)?;
let mut expected: Map<String, Value> = Map::new();
expected.insert(String::from("with_a_garbage"), Value::String(String::from("value")));
let res = get_table(&fx_value, String::from("tab"))?;
assert_eq!(res, &expected);
Ok(())
}
#[test]
fn read_config_string_ok() -> Result<()> {
let fx_sample_config_string = "
[all]
gitea_url = \"http://localhost:3000\"
token = \"fake-token\"
[\"/home/robert/projects/gt-tool\"]
owner = \"robert\"
repo = \"gt-tool\"
[\"/home/robert/projects/rcalc\"]
owner = \"jamis\"
repo = \"rcalc\"
[\"/home/robert/projects/rcalc-builders\"]
owner = \"jamis\"
repo = \"rcalc\"
";
let fx_expected_struct = WholeFile {
all: PartialConfig {
project_path: None,
gitea_url: Some(String::from("http://localhost:3000")),
owner: None,
repo: None,
token: Some(String::from("fake-token"))
},
project_overrides: vec![
PartialConfig {
project_path: Some(String::from("/home/robert/projects/gt-tool")),
gitea_url: None,
owner: Some(String::from("robert")),
repo: Some(String::from("gt-tool")),
token: None,
},
PartialConfig {
project_path: Some(String::from("/home/robert/projects/rcalc")),
gitea_url: None,
owner: Some(String::from("jamis")),
repo: Some(String::from("rcalc")),
token: None,
},
PartialConfig {
project_path: Some(String::from("/home/robert/projects/rcalc-builders")),
gitea_url: None,
owner: Some(String::from("jamis")),
repo: Some(String::from("rcalc")),
token: None,
},
],
};
let conf = read_conf_str(fx_sample_config_string)?;
println!(" ->> Test conf: {:?}", conf);
println!(" ->> Ref conf: {:?}", fx_expected_struct);
assert_eq!(conf, fx_expected_struct);
Ok(())
}
/* TODO: Improve semantics around reading an empty string
An empty config string will result in Error::NoSuchTable when "[all]"
is retrieved. But this will *also* happen when other configs are present,
but "[all]" isn't. Do I treat these as valid configurations, using some
hard-coded default as the fallback? Or do I reject configs that don't have
an all-table?
*/
#[test]
fn read_config_string_empty() {
let fx_sample_cfg = "";
let conf = read_conf_str(fx_sample_cfg);
assert!(conf.is_err());
}
}

View File

@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
pub mod api;
pub mod cli;
pub mod config;
pub mod structs;
#[derive(Debug, Deserialize, Serialize)]