25 Commits

Author SHA1 Message Date
c5c5598fb7 Mark v3.0.1 patch
All checks were successful
/ Compile and upload a release build (release) Successful in 54s
Barely anything has changed, but the package *is* different. v3 is from
months back and is missing information that Crates.io kinda wants.
2025-11-13 16:55:12 -06:00
9e47cb72d5 Remove the comments tracking Debian-specific deps
The Debian 12 dependency versions can go away since I'm no longer
targetting it. The Debian Sid versions haven't been checked in months,
and I'm not actually targetting it *either* (never have been). They can
both go away.
2025-11-13 16:40:01 -06:00
ff2286f44b Add metadata required for publishing to crates.io
I'm not sure they're required. I sure hope not because I don't have a
homepage, and the guide says not to reuse the repo URL there.
2025-11-13 16:36:54 -06:00
d982f42ae7 Update dependency versions for Debian 13 "Trixie"
I'm deciding that I'll only support the latest stable release of
Debian. Somehow I doubt anyone is using this tool, and those who do are
unlikely to need an even longer support window than Debian's stable
release period.

This change bumps the dependencies to match those available in Debian
13. Some upgrades would have already happened, while others are blocked
by the SemVer rules.

For example, Clap 4.0 to 4.5 would happen automatically, but TOML 0.5 to
0.8 would not.
2025-09-11 14:12:12 -05:00
641efc3bf7 Update automation workflow with new CLI args
All checks were successful
/ Compile and upload a release build (release) Successful in 1m10s
2025-07-22 09:41:54 -05:00
144fba5373 Bump crate version to v3.0.0
Some checks failed
/ Compile and upload a release build (release) Failing after 49s
2025-07-21 16:37:37 -05:00
7f35b808e5 Lint and format 2025-07-21 16:37:14 -05:00
00edaf87ce Mark the file-format and search-path conf sections
There are two concepts here and that should be more clearly indicated.

Introduce the file format with some examples, then talk about where
those files are found.
2025-07-21 16:21:10 -05:00
250140a954 Rephrase the all-projects setting introduction 2025-07-21 16:20:48 -05:00
b290a8b1d6 Drop the "no-repo" comment in TOML example
It's not relevant to the example and might confuse readers as a result.
2025-07-21 16:18:44 -05:00
b952e40060 Revise explanation of --project option
I need to introduce the idea that "projects" are actually file paths,
and that these paths are the keys for the key-value stores that are the
config files.

...but without saying "HashMap" because that's really an implementation
detail.
2025-07-21 16:14:47 -05:00
4b9257a9a7 Rename remaining CLI arg sections
The previous text was pretty ugly and not particularly useful to catch
the eye when looking for relevant sections.
2025-07-21 16:13:46 -05:00
d34eda77dc Delete the old CLI option sections 2025-07-21 16:12:56 -05:00
3315c18ed2 New 'authentication' section
The auth tokens can now be loaded from the config files, so I need to
mention that.

I took the opportunity to revise the explanation of when auth is
required. Now it has a more obvious example of how it depends on
instance configuration.
2025-07-21 16:09:12 -05:00
0e7bca80cb Create a short, complete explanation of req. info.
I don't need to have nearly so much information explaining how to use
optional command line arguments. The quirk about the "repo" needing to
be a URL fragment somewhat justified the extra explanation, but that's
gone now.

Instead, a short, up-front section stating which bits are required and
where the program will try to get them.
2025-07-21 16:08:08 -05:00
5bd2862498 Update usage printout 2025-07-21 15:47:23 -05:00
333636b524 Revise help text for CLI "--project" arg 2025-07-21 14:57:18 -05:00
e954a2b09a Drop notice about CLI not having "repo" & "owner" 2025-07-21 14:54:16 -05:00
da8f008f1a Use current-dir as final fallback repo name
It all falls into place! I had been dreading doing this bit, but after
updating the usage guide I realized the CLI args should be split, too.
Which finally means that I can just glue on the PWD name as a final
fallback for the repo name.

Try the args, then the config file(s), then PWD. If nothing works, the
user is in a world of hurt. Bail out.
2025-07-21 14:48:30 -05:00
7c0966be30 Split the owner and repo args apart in CLI parser 2025-07-21 14:21:13 -05:00
c1019afa7a Write configuration guide in the README 2025-07-21 14:04:04 -05:00
1a619d7bb4 Update CLI usage guide, add project lookup guide
There's a new inconsistency, however. The previous URL and FQRN
arguments are no longer mandatory but their description makes it seem
as though they are.
2025-07-21 14:04:01 -05:00
fc0d1b569c Add a project path CLI option 2025-07-21 11:56:20 -05:00
8cfc6605c9 Mark pre-release 3.0.0-alpha.1
The configuration file loading is complete and seems to work the way I
expect. I still need to do an improved project guessing system, and it
would be smart to allow the user to explicitly enter a config file to
use.
2025-07-21 10:58:37 -05:00
0e814b86a1 Fix some clippy lints 2025-07-21 10:52:20 -05:00
7 changed files with 151 additions and 61 deletions

View File

@@ -22,7 +22,9 @@ jobs:
- name: Upload the program (using itself!) - name: Upload the program (using itself!)
run: > run: >
target/release/gt-tool-${{ github.ref_name }}-$(arch) target/release/gt-tool-${{ github.ref_name }}-$(arch)
-u ${{ vars.DEST_GITEA }} -r ${{ vars.DEST_REPO }} -u ${{ vars.DEST_GITEA }}
-o ${{ vars.DEST_OWNER }}
-r ${{ vars.DEST_REPO }}
upload-release upload-release
"${{ github.ref_name }}" "${{ github.ref_name }}"
target/release/gt-tool-${{ github.ref_name }}-$(arch) target/release/gt-tool-${{ github.ref_name }}-$(arch)

View File

@@ -1,23 +1,18 @@
[package] [package]
name = "gt-tool" name = "gt-tool"
version = "2.2.0" version = "3.0.1"
edition = "2024" edition = "2024"
license = "GPL-3.0-only"
description = "CLI tools for interacting with the Gitea API. Mainly for attaching files to releases."
# homepage = "" I have no website for a project home page :(
repository = "https://git.gelvin.dev/robert/gt-tool"
readme = "README.md"
[dependencies] [dependencies]
clap = { version = "4.0.7", features = ["derive", "env"] } clap = { version = "4.5.23", features = ["derive", "env"] }
colored = "2.0.0" colored = "2.2.0"
itertools = "0.10.0" itertools = "0.13.0"
reqwest = { version = "0.11.13", features = ["json", "stream", "multipart"] } reqwest = { version = "0.12.15", features = ["json", "stream", "multipart"] }
serde = { version = "1.0.152", features = ["derive"] } serde = { version = "1.0.217", features = ["derive"] }
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.43.1", features = ["macros", "rt-multi-thread"] }
toml = "0.5" toml = "0.8.19"
# Packages available in Debian (Sid)
# clap = "4.5.23"
# reqwest = "0.12.15"
# tokio = "1.43.1"
# Debian (Bookworm)
# clap = "4.0.32"
# reqwest = "0.11.13"
# tokio = "1.24.2"

111
README.md
View File

@@ -5,7 +5,7 @@ CLI tools for interacting with the Gitea API. Use interactively to talk to your
## Usage ## Usage
```txt ```txt
Usage: gt-tools --url <GITEA_URL> --repo <REPO> <COMMAND> Usage: gt-tool [OPTIONS] <COMMAND>
Commands: Commands:
list-releases list-releases
@@ -14,35 +14,52 @@ Commands:
help Print this message or the help of the given subcommand(s) help Print this message or the help of the given subcommand(s)
Options: Options:
-u, --url <GITEA_URL> [env: GTTOOL_GITEA_URL=] -u, --url <GITEA_URL> [env: GTTOOL_GITEA_URL=]
-r, --repo <REPO> [env: GTTOOL_FQRN=] -o, --owner <OWNER> [env: GTTOOL_OWNER=]
-h, --help Print help -r, --repo <REPO> [env: GTTOOL_REPO=]
-V, --version Print version -p, --project <PROJECT> Path to project (relative or absolute). Used to override configuration selection.
-h, --help Print help
-V, --version Print version
``` ```
### Required Information
To function, this program requires knowledge of these items:
- Gitea URL
- Owner of repository
- Repository name
This info will be gathered from these locations, in order of priority:
1. CLI argument
2. Environment variable
3. Configuration files
It's worth noting that the "owner" is the entity that controls the repo on the Gitea instance. This will be the first part of the route in the URL: `http://demo.gitea.com/{owner}`.
Likewise, the "repo" is what ever the Gitea instance thinks it's called -- which doesn't have to match anyone's local copy! It will be the second part of the route in the URL: `http://demo.gitea.com/{owner}/{repo}`.
### Authentication ### Authentication
Authentication is token-based via environment variable `RELEASE_KEY_GITEA`. Authentication is token-based. There is no CLI option to prevent the token from appearing in any command logs.
Ensure your token has the appropriate access for your usage. This depends on what you're doing and how your Gitea instance is configured, so you'll have to figure it out for yourself. In order of priority, the token is loaded from:
Most likely, you will need a token with "repository: read-and-write" permissions. See Gitea's documentation on [token scopes](https://docs.gitea.com/development/oauth2-provider#scopes) for more. 1. The environment variable `RELEASE_KEY_GITEA`
2. Config files, key `token`
### `<GITEA_URL>`: Whether or not it is required depends on how your Gitea instance and the repositories inside are configured. For example, a default Gitea configuration will allow unauthenticated users to see public repositories but not make any changes. This means no token is required to run `gt-tool list-releases`, while `gt-tool upload-release` *will* require a token.
The Gitea server URL must be provided with `--url` or `-u` on the command line, or via the environment variable `GTTOOL_GITEA_URL`. Use the base URL for your Gitea instance. For details, see Gitea's documentation on [token scopes](https://docs.gitea.com/development/oauth2-provider#scopes).
E.g.: Using the Gitea org's demo instance, it would be: `--url "https://demo.gitea.com/"` ### The `--project` option
### `<REPO>`: Settings retrieved from config files are selected based on the project's path. By default, the current directory will be used. In case that guess is incorrect, this option can be specified with another path.
The repository name must be provided with `--repo` or `-u` on the command line, or via the environment variable `GTTOOL_GITEA_FQRN` ("fully qualified repo name"). Use the format `<owner>/<repo>`, which is the route immediately following the GITEA_URL base. This is how GitHub and Gitea identify repos in the URL, and how Golang locates it's modules, so this tool does the same. See [configuration](#configuration) for details on format and file locations.
E.g.: `--repo "go-gitea/gitea"` would name the Gitea repo belonging to the go-gitea organization. ### Commands:
### `<COMMAND>`:
One of these, defaults to `help`:
| Command | Description | | Command | Description |
|-|-| |-|-|
@@ -51,3 +68,61 @@ One of these, defaults to `help`:
| upload-release | Uploads one-or-more files to an existing release, identified by it's tag name. | | upload-release | Uploads one-or-more files to an existing release, identified by it's tag name. |
| help | prints the help text (the usage summary above). | | help | prints the help text (the usage summary above). |
## Configuration
Instead of specifying everything on the command line every single run, some TOML files can be used to persist project settings.
> Exporting some environment variables would be similar, but that would be *more* annoying when working on multiple projects. One would have to constantly re-export the settings or use two shells. But then there's the issue of losing track of which shell has which settings.
### File Format
Settings are retrieved from a table named the same as the project's path on disk. For example, gt_tool itself could have an entry as follows:
```toml
["/home/robert/projects/gt-tool"]
gitea_url = "https://demo.gitea.com/"
owner = "dummy"
repo = "gt-tool"
token = "fake-token"
```
Sometimes one may want to apply a setting to all projects. For this, they can use the special `[all]` table.
```toml
[all]
gitea_url = "https://demo.gitea.com/"
```
Since the more-specific settings are preferred, these can be combined to have an override effect.
```toml
[all]
gitea_url = "https://demo.gitea.com/"
owner = "robert"
token = "fake-token"
# Override Gitea target so I can test my uploads privately.
["/home/robert/projects/gt-tool"]
gitea_url = "http://localhost:3000"
repo = "gt-tool"
```
### Search Paths
Similar to how unspecified project settings will fall back to those in the "`[all]`" table, whole files will fall back to other, lower priority files.
1. First, each dir in `$XDG_CONFIG_DIRS` is scanned for a `gt-tool.toml` file.
2. Then, `/etc/gt-tool.toml`.
> All config files **MUST** be named named `gt-tool.toml`.
### Recognized Keys
| Key | Description |
|-|-|
| gitea_url | URL of the Gitea server. Same as `-u`, `--url`, and `$GTTOOL_GITEA_URL`. |
| owner | Owner of the repository (individual, or organization). Combined with "repo" key to produce the fully-qualified-repo-name. Front-half of `-r`, `--repo`, and `$GTTOOL_FQRN` |
| repo | Name of the repository on the Gitea server. Combined with "owner" key to produce the fully-qualified-repo-name. Back-half of `-r`, `--repo`, and `$GTTOOL_FQRN` |
| token | Gitea auth token, exactly the same as `$RELEASE_KEY_GITEA` |
Additional keys are quietly ignored. The config loading is done by querying a HashMap, so anything not touched doesn't get inspected. The only requirements are that the file is valid TOML, and that these keys are all strings.h

View File

@@ -5,8 +5,16 @@ use clap::{Parser, Subcommand};
pub struct Args { pub struct Args {
#[arg(short = 'u', long = "url", env = "GTTOOL_GITEA_URL")] #[arg(short = 'u', long = "url", env = "GTTOOL_GITEA_URL")]
pub gitea_url: Option<String>, pub gitea_url: Option<String>,
#[arg(short = 'r', long = "repo", env = "GTTOOL_FQRN")] #[arg(short = 'o', long = "owner", env = "GTTOOL_OWNER")]
pub owner: Option<String>,
#[arg(short = 'r', long = "repo", env = "GTTOOL_REPO")]
pub repo: Option<String>, pub repo: Option<String>,
#[arg(
short = 'p',
long = "project",
help = "Path to project (relative or absolute). Used to override configuration selection."
)]
pub project: Option<String>,
#[command(subcommand)] #[command(subcommand)]
pub command: Commands, pub command: Commands,

View File

@@ -110,15 +110,15 @@ pub fn get_config(
// 3b. or default, if the table couldn't be found. // 3b. or default, if the table couldn't be found.
.or(Ok(PartialConfig::default())) .or(Ok(PartialConfig::default()))
// 4. assemble a 2-tuple of PartialConfigs by... // 4. assemble a 2-tuple of PartialConfigs by...
.and_then(|proj| { .map(|proj| {
Ok(( (
// 4-1. Passing in the project-specific PartialConfig // 4-1. Passing in the project-specific PartialConfig
proj.project_path(project), proj.project_path(project),
// 4-2. Getting and converting to PartialConfig, or returning any Err() if one appears. // 4-2. Getting and converting to PartialConfig, or returning any Err() if one appears.
get_table(cfg_table, "all") get_table(cfg_table, "all")
.and_then(PartialConfig::try_from) .and_then(PartialConfig::try_from)
.unwrap_or(PartialConfig::default()), .unwrap_or(PartialConfig::default()),
)) )
}) })
.map(|pair| pair.0.merge(pair.1)) .map(|pair| pair.0.merge(pair.1))
}) })

View File

@@ -23,6 +23,8 @@ pub enum Error {
Placeholder, // TODO: Enumerate error modes Placeholder, // TODO: Enumerate error modes
MissingGiteaUrl, // the gitea URL wasn't specified on the CLI, env, or config file. MissingGiteaUrl, // the gitea URL wasn't specified on the CLI, env, or config file.
MissingRepoFRQN, // either the owner, repo, or both weren't specified in the loaded PartialConfig MissingRepoFRQN, // either the owner, repo, or both weren't specified in the loaded PartialConfig
MissingRepoOwner,
MissingRepoName,
WrappedConfigErr(config::Error), WrappedConfigErr(config::Error),
WrappedReqwestErr(reqwest::Error), WrappedReqwestErr(reqwest::Error),
MissingAuthToken, MissingAuthToken,

View File

@@ -1,4 +1,4 @@
use std::path; use std::path::{self, PathBuf};
use gt_tool::cli::Args; use gt_tool::cli::Args;
use gt_tool::structs::release::{CreateReleaseOption, Release}; use gt_tool::structs::release::{CreateReleaseOption, Release};
@@ -12,12 +12,15 @@ use reqwest::header::ACCEPT;
async fn main() -> Result<(), gt_tool::Error> { async fn main() -> Result<(), gt_tool::Error> {
let args = Args::parse(); let args = Args::parse();
// TODO: Heuristics to guess project path let project_path =
// See issue #8: https://git.gelvin.dev/robert/gt-tool/issues/8 args.project
let pwd = std::env::current_dir() .map(PathBuf::from)
.map_err(|_e| gt_tool::Error::WrappedConfigErr(gt_tool::config::Error::CouldntReadFile))?; .unwrap_or(std::env::current_dir().map_err(|_e| {
gt_tool::Error::WrappedConfigErr(gt_tool::config::Error::CouldntReadFile)
})?);
let config = gt_tool::config::get_config( let config = gt_tool::config::get_config(
pwd.to_str() project_path
.to_str()
.expect("I assumed the path can be UTF-8, but that didn't work out..."), .expect("I assumed the path can be UTF-8, but that didn't work out..."),
gt_tool::config::default_paths(), gt_tool::config::default_paths(),
)?; )?;
@@ -27,21 +30,19 @@ async fn main() -> Result<(), gt_tool::Error> {
.gitea_url .gitea_url
.or(config.gitea_url) .or(config.gitea_url)
.ok_or(gt_tool::Error::MissingGiteaUrl)?; .ok_or(gt_tool::Error::MissingGiteaUrl)?;
// Config files split the repo FQRN into "owner" and "repo" (confusing naming, sorry)
// These must be merged back together and passed along. let owner = args
let conf_fqrn = config
.owner .owner
.ok_or(gt_tool::Error::MissingRepoFRQN) .or(config.owner)
.and_then(|mut own| { .ok_or(gt_tool::Error::MissingRepoOwner)?;
let repo = config.repo.ok_or(gt_tool::Error::MissingRepoFRQN)?;
own.push_str("/"); let repo = args
own.push_str(&repo);
Ok(own)
});
let repo_fqrn = args
.repo .repo
.ok_or(gt_tool::Error::MissingRepoFRQN) .or(config.repo)
.or(conf_fqrn)?; .or_else(infer_repo)
.ok_or(gt_tool::Error::MissingRepoName)?;
let repo_fqrn = format!("{owner}/{repo}");
let mut headers = reqwest::header::HeaderMap::new(); let mut headers = reqwest::header::HeaderMap::new();
headers.append(ACCEPT, header::HeaderValue::from_static("application/json")); headers.append(ACCEPT, header::HeaderValue::from_static("application/json"));
@@ -67,7 +68,7 @@ async fn main() -> Result<(), gt_tool::Error> {
releases.iter().rev().map(|release| release.colorized()), releases.iter().rev().map(|release| release.colorized()),
String::from(""), String::from(""),
) )
.map(|release| println!("{}", release)) .map(|release| println!("{release}"))
.fold((), |_, _| ()); .fold((), |_, _| ());
} }
gt_tool::cli::Commands::CreateRelease { gt_tool::cli::Commands::CreateRelease {
@@ -171,3 +172,10 @@ fn match_release_by_tag(tag: &String, releases: Vec<Release>) -> Option<Release>
} }
release release
} }
fn infer_repo() -> Option<String> {
let pwd = std::env::current_dir().ok()?;
let file_name = pwd.file_name()?;
let file_name_string = file_name.to_str()?;
Some(String::from(file_name_string))
}