Compare commits
118 Commits
debian/1.0
...
deb/trixie
| Author | SHA1 | Date | |
|---|---|---|---|
| 8381514640 | |||
| a9168d8851 | |||
| f7b54b9605 | |||
| fa8f590019 | |||
| 89179e49e7 | |||
| 47b91a1e42 | |||
| 4778c6f362 | |||
| 2022da671e | |||
| 25af28e97c | |||
| 02cabfeb1a | |||
| 54b54b42d7 | |||
| 21799aa247 | |||
| c5c5598fb7 | |||
| 9e47cb72d5 | |||
| ff2286f44b | |||
| d982f42ae7 | |||
| daf9ec6ae6 | |||
| fc44129039 | |||
| a88e44ab97 | |||
| 17165a58bb | |||
| 641efc3bf7 | |||
| 144fba5373 | |||
| 7f35b808e5 | |||
| 00edaf87ce | |||
| 250140a954 | |||
| b290a8b1d6 | |||
| b952e40060 | |||
| 4b9257a9a7 | |||
| d34eda77dc | |||
| 3315c18ed2 | |||
| 0e7bca80cb | |||
| 5bd2862498 | |||
| 333636b524 | |||
| e954a2b09a | |||
| da8f008f1a | |||
| 7c0966be30 | |||
| c1019afa7a | |||
| 1a619d7bb4 | |||
| fc0d1b569c | |||
| 8cfc6605c9 | |||
| 0e814b86a1 | |||
| 0e3aa16e00 | |||
| 04dd333d72 | |||
| 13ef1d25eb | |||
| 56b0580a9a | |||
| 46d8618e74 | |||
| 73363718c3 | |||
| 5b8a09e9ca | |||
| 3453f64312 | |||
| 63d0a868ec | |||
| 4e9a5dd25b | |||
| ce480306e0 | |||
| 6ca279de49 | |||
| 64215cefcc | |||
| cf9b37fe99 | |||
| ed76fa67ff | |||
| 2e2c54d538 | |||
| 2b47460258 | |||
| b26a594cc8 | |||
| 246987fa68 | |||
| 551297f46b | |||
| 912a7283fd | |||
| 247c06dd9e | |||
| cb314a8b4c | |||
| 277f638c60 | |||
| 626973d2bc | |||
| 28539f54cc | |||
| 5ce20adf2e | |||
| fc1e20185e | |||
| 15593204e0 | |||
| 330985940f | |||
| 213e0b4f4a | |||
| d27bea2c43 | |||
| 30d8bcc6de | |||
| b2b9c8b9d9 | |||
| f6bab75644 | |||
| f9673b715f | |||
| 820a9daaed | |||
| 8d71b81271 | |||
| 075a2ee921 | |||
| 8eacb510a2 | |||
| a23bdf3e34 | |||
| 17bde93259 | |||
| cdb312fe61 | |||
| cfaa0ceb3f | |||
| c0a0181074 | |||
| 119831481e | |||
| 7246c7afb6 | |||
| 84eaaa1dbd | |||
| c9dda5760c | |||
| 336f1453b9 | |||
| f068e8233e | |||
| d4ef21e243 | |||
| d94c350cde | |||
| 8120cb0489 | |||
| b82cfdb822 | |||
| ea046c929f | |||
| 135acf09b7 | |||
| bbae6b4395 | |||
| 2c03c5ba4d | |||
| 136c051c82 | |||
| 6a9ec25d1a | |||
| 395ce8a804 | |||
| 616be020f0 | |||
| a0ba8e7ea8 | |||
| 324c7e67a7 | |||
| 984974c240 | |||
| 88cafc096f | |||
| b200785e71 | |||
| 8246337ae4 | |||
| 06795df3f7 | |||
| a5f6335b5f | |||
| 4a0addda67 | |||
| 0c70b584ba | |||
| 8a11c21b73 | |||
| d42cbbc1ec | |||
| 96e9ff4ce6 | |||
| 6bdad44cc6 |
@@ -8,6 +8,11 @@ jobs:
|
|||||||
name: Compile and upload a release build
|
name: Compile and upload a release build
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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
|
- name: Install Rust Stable
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
- name: Build binary crate
|
- name: Build binary crate
|
||||||
@@ -17,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)
|
||||||
|
|||||||
28
Cargo.toml
28
Cargo.toml
@@ -1,20 +1,18 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gt-tool"
|
name = "gt-tool"
|
||||||
version = "1.0.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"] }
|
||||||
reqwest = { version = "0.11.13", features = ["json", "stream", "multipart"] }
|
colored = "2.2.0"
|
||||||
serde = { version = "1.0.152", features = ["derive"] }
|
itertools = "0.13.0"
|
||||||
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] }
|
reqwest = { version = "0.12.15", features = ["json", "stream", "multipart"] }
|
||||||
|
serde = { version = "1.0.217", features = ["derive"] }
|
||||||
# Packages available in Debian (Sid)
|
tokio = { version = "1.43.1", features = ["macros", "rt-multi-thread"] }
|
||||||
# clap = "4.5.23"
|
toml = "0.8.19"
|
||||||
# reqwest = "0.12.15"
|
|
||||||
# tokio = "1.43.1"
|
|
||||||
|
|
||||||
# Debian (Bookworm)
|
|
||||||
# clap = "4.0.32"
|
|
||||||
# reqwest = "0.11.13"
|
|
||||||
# tokio = "1.24.2"
|
|
||||||
|
|||||||
116
README.md
116
README.md
@@ -1,11 +1,11 @@
|
|||||||
# 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.
|
CLI tools for interacting with the Gitea API. Use interactively to talk to your Gitea instance, or automatically via a CI/CD pipeline.
|
||||||
|
|
||||||
## 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,14 +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). |
|
||||||
|
|
||||||
## Unit Testing
|
## Configuration
|
||||||
|
|
||||||
The unit test~~s~~ require a Gitea server to execute against. This information is supplied by environment variables rather than on the command line, but it is otherwise exactly the same usage.
|
Instead of specifying everything on the command line every single run, some TOML files can be used to persist project settings.
|
||||||
|
|
||||||
| Variable | Description |
|
> 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 |
|
||||||
|-|-|
|
|-|-|
|
||||||
| TEST_GITEA_SERVER | Server URL, match `-u`, `--url` |
|
| gitea_url | URL of the Gitea server. Same as `-u`, `--url`, and `$GTTOOL_GITEA_URL`. |
|
||||||
| TEST_GITEA_REPO | Owner + repo name, match `-u` `--repo` |
|
| 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` |
|
||||||
| TEST_GITEA_KEY | API key, match `RELEASE_KEY_GITEA`. The use of a new variable for the API token is to help avoid accidentally touching a production environment during test execution. |
|
| 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` |
|
||||||
| TEST_GITEA_RELEASE_TAG | Git tag used to identify the Release. Same as `upload-release`'s positional argument `<TAG_NAME>`. |
|
| 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
|
||||||
|
|||||||
1
debian/cargo-checksum.json
vendored
Normal file
1
debian/cargo-checksum.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"package":"62e0ece55a4e0150ab3a3f7f5299e6eb113134975100cddde5a68dfcbb5e1a5c","files":{}}
|
||||||
141
debian/changelog
vendored
141
debian/changelog
vendored
@@ -1,4 +1,143 @@
|
|||||||
gt-tool (1.0.0-1) UNRELEASED; urgency=low
|
gt-tool (3.0.1-2) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Fix: debian/copyright was using old package name.
|
||||||
|
* Add a cargo-checksum.json, remove its fake
|
||||||
|
* Remove bogus pre-step inserts
|
||||||
|
* Strip out all the imports and exports
|
||||||
|
* Drop a copyright notice (!)
|
||||||
|
* Adopt debian/control contents from debcargo output
|
||||||
|
* Adopt debcargo auto-gen'd testing stuff
|
||||||
|
|
||||||
|
-- Robert Garrett <robertgarrett404@gmail.com> Thu, 13 Nov 2025 22:07:35 -0600
|
||||||
|
|
||||||
|
gt-tool (3.0.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Update dependency versions for Debian 13 "Trixie"
|
||||||
|
* Add metadata required for publishing to crates.io
|
||||||
|
* Remove the comments tracking Debian-specific deps
|
||||||
|
* Mark v3.0.1 patch
|
||||||
|
* Update gbp.conf debian-branch to "deb/trixie"
|
||||||
|
* Drop entirety of old patch set
|
||||||
|
|
||||||
|
-- Robert Garrett <robertgarrett404@gmail.com> Thu, 13 Nov 2025 17:12:26 -0600
|
||||||
|
|
||||||
|
gt-tool (3.0.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Make the README title singular
|
||||||
|
* Add a Cargo.toml & Git tag version comparison
|
||||||
|
* Scaffold the new config module
|
||||||
|
* Add property-get utility function
|
||||||
|
* Add a get-table util function
|
||||||
|
* Util fn's can use anything that impl's ToString
|
||||||
|
* Util to get sometimes-empty config property
|
||||||
|
* Add partial- and whole- config structs
|
||||||
|
* Prototype load-config function
|
||||||
|
* Put the per-project test expects in for lconf()
|
||||||
|
* Finish `fn lconf()`. Project-specific vals load
|
||||||
|
* Create-and-assign struct to whole.all
|
||||||
|
* Use the get_table util to extract "[all]" table
|
||||||
|
* Extract PartCfg readers to a try_from impl
|
||||||
|
* Add a builder-pattern proj-path setter, for flavor
|
||||||
|
* Assert empty conf str is an error, TODO: semantics
|
||||||
|
* Rename the config-string-reading function
|
||||||
|
* Externalize the test table
|
||||||
|
* Remove some debug prints
|
||||||
|
* Signature & tests for fn load_from_file()
|
||||||
|
* Implement the load_from_file function
|
||||||
|
* "Merge" method on PartialConfig
|
||||||
|
* Complete the public `get_config()` function
|
||||||
|
* Pass in search files rather than generating them
|
||||||
|
* Make default search paths available as util fn
|
||||||
|
* Remove `WholeFile` struct & anything that uses it
|
||||||
|
* Autoformat
|
||||||
|
* Cargo clippy fixes
|
||||||
|
* Delete a now-solved FIXME comment
|
||||||
|
* Make the URL and Repo FQRN CLI args optional
|
||||||
|
* Wire in the conf file loading, assume PWD project
|
||||||
|
* Add more unit tests for the config loader
|
||||||
|
* Add test for skipping unavailable conf files
|
||||||
|
* Fix config unit tests: project path is set!
|
||||||
|
* Add docstring for PartialConfig::try_from()
|
||||||
|
* Fix: use empty PartialConfig if proj conf missing
|
||||||
|
* Fix: use default "[all]" if one isn't present
|
||||||
|
* Another autoformat
|
||||||
|
* Fix some clippy lints
|
||||||
|
* Mark pre-release 3.0.0-alpha.1
|
||||||
|
* Add a project path CLI option
|
||||||
|
* Update CLI usage guide, add project lookup guide
|
||||||
|
* Write configuration guide in the README
|
||||||
|
* Split the owner and repo args apart in CLI parser
|
||||||
|
* Use current-dir as final fallback repo name
|
||||||
|
* Drop notice about CLI not having "repo" & "owner"
|
||||||
|
* Revise help text for CLI "--project" arg
|
||||||
|
* Update usage printout
|
||||||
|
* Create a short, complete explanation of req. info.
|
||||||
|
* New 'authentication' section
|
||||||
|
* Delete the old CLI option sections
|
||||||
|
* Rename remaining CLI arg sections
|
||||||
|
* Revise explanation of `--project` option
|
||||||
|
* Drop the "no-repo" comment in TOML example
|
||||||
|
* Rephrase the all-projects setting introduction
|
||||||
|
* Mark the file-format and search-path conf sections
|
||||||
|
* Lint and format
|
||||||
|
* Bump crate version to v3.0.0
|
||||||
|
* Update automation workflow with new CLI args
|
||||||
|
* Add new upstream dependency to debian/control
|
||||||
|
* Rediff patches
|
||||||
|
|
||||||
|
-- Robert Garrett <robertgarrett404@gmail.com> Tue, 22 Jul 2025 09:54:28 -0500
|
||||||
|
|
||||||
|
gt-tool (2.2.0-2) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Fix: "Source:" URL in debian/copyright
|
||||||
|
* Fix typo in copyright comment
|
||||||
|
|
||||||
|
-- Robert Garrett <robertgarrett404@gmail.com> Sun, 13 Jul 2025 16:59:25 -0500
|
||||||
|
|
||||||
|
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.
|
* Experimental release.
|
||||||
|
|
||||||
|
|||||||
70
debian/control
vendored
70
debian/control
vendored
@@ -1,25 +1,83 @@
|
|||||||
Source: gt-tool
|
Source: gt-tool
|
||||||
Maintainer: Robert Garrett <robertgarrett404@gmail.com>
|
Maintainer: Robert Garrett <robertgarrett404@gmail.com>
|
||||||
Section: misc
|
Section: rust
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Standards-Version: 4.6.2
|
Standards-Version: 4.6.2
|
||||||
Build-Depends:
|
Build-Depends:
|
||||||
debhelper-compat (= 13),
|
debhelper-compat (= 13),
|
||||||
dh-cargo,
|
dh-sequence-cargo
|
||||||
librust-clap-dev,
|
Build-Depends-Arch:
|
||||||
librust-reqwest-dev,
|
cargo:native,
|
||||||
librust-tokio-dev,
|
rustc:native,
|
||||||
librust-serde-dev,
|
libstd-rust-dev,
|
||||||
|
librust-clap-4+default-dev (>= 4.5.23-~~),
|
||||||
|
librust-clap-4+derive-dev (>= 4.5.23-~~),
|
||||||
|
librust-clap-4+env-dev (>= 4.5.23-~~),
|
||||||
|
librust-colored-2+default-dev (>= 2.2.0-~~),
|
||||||
|
librust-itertools-0.13+default-dev,
|
||||||
|
librust-reqwest-0.12+default-dev (>= 0.12.15-~~),
|
||||||
|
librust-reqwest-0.12+json-dev (>= 0.12.15-~~),
|
||||||
|
librust-reqwest-0.12+multipart-dev (>= 0.12.15-~~),
|
||||||
|
librust-reqwest-0.12+stream-dev (>= 0.12.15-~~),
|
||||||
|
librust-serde-1+default-dev (>= 1.0.217-~~),
|
||||||
|
librust-serde-1+derive-dev (>= 1.0.217-~~),
|
||||||
|
librust-tokio-1+default-dev (>= 1.43.1-~~),
|
||||||
|
librust-tokio-1+macros-dev (>= 1.43.1-~~),
|
||||||
|
librust-tokio-1+rt-multi-thread-dev (>= 1.43.1-~~),
|
||||||
|
librust-toml-0.8+default-dev (>= 0.8.19-~~)
|
||||||
Homepage: https://git.gelvin.dev/robert/gt-tool
|
Homepage: https://git.gelvin.dev/robert/gt-tool
|
||||||
Vcs-Git: 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
|
Vcs-Browser: https://git.gelvin.dev/robert/gt-tool
|
||||||
Rules-Requires-Root: no
|
Rules-Requires-Root: no
|
||||||
|
X-Cargo-Crate: gt-tool
|
||||||
|
|
||||||
|
Package: librust-gt-tool-dev
|
||||||
|
Architecture: any
|
||||||
|
Multi-Arch: same
|
||||||
|
Depends:
|
||||||
|
${misc:Depends},
|
||||||
|
librust-clap-4+default-dev (>= 4.5.23-~~),
|
||||||
|
librust-clap-4+derive-dev (>= 4.5.23-~~),
|
||||||
|
librust-clap-4+env-dev (>= 4.5.23-~~),
|
||||||
|
librust-colored-2+default-dev (>= 2.2.0-~~),
|
||||||
|
librust-itertools-0.13+default-dev,
|
||||||
|
librust-reqwest-0.12+default-dev (>= 0.12.15-~~),
|
||||||
|
librust-reqwest-0.12+json-dev (>= 0.12.15-~~),
|
||||||
|
librust-reqwest-0.12+multipart-dev (>= 0.12.15-~~),
|
||||||
|
librust-reqwest-0.12+stream-dev (>= 0.12.15-~~),
|
||||||
|
librust-serde-1+default-dev (>= 1.0.217-~~),
|
||||||
|
librust-serde-1+derive-dev (>= 1.0.217-~~),
|
||||||
|
librust-tokio-1+default-dev (>= 1.43.1-~~),
|
||||||
|
librust-tokio-1+macros-dev (>= 1.43.1-~~),
|
||||||
|
librust-tokio-1+rt-multi-thread-dev (>= 1.43.1-~~),
|
||||||
|
librust-toml-0.8+default-dev (>= 0.8.19-~~)
|
||||||
|
Provides:
|
||||||
|
librust-gt-tool+default-dev (= ${binary:Version}),
|
||||||
|
librust-gt-tool-3-dev (= ${binary:Version}),
|
||||||
|
librust-gt-tool-3+default-dev (= ${binary:Version}),
|
||||||
|
librust-gt-tool-3.0-dev (= ${binary:Version}),
|
||||||
|
librust-gt-tool-3.0+default-dev (= ${binary:Version}),
|
||||||
|
librust-gt-tool-3.0.1-dev (= ${binary:Version}),
|
||||||
|
librust-gt-tool-3.0.1+default-dev (= ${binary:Version})
|
||||||
|
Description: CLI tools for interacting with the Gitea API - Rust source code
|
||||||
|
Mainly for attaching files to releases.
|
||||||
|
.
|
||||||
|
Source code for Debianized Rust crate "gt-tool"
|
||||||
|
|
||||||
Package: gt-tool
|
Package: gt-tool
|
||||||
Architecture: any
|
Architecture: any
|
||||||
Depends:
|
Depends:
|
||||||
${misc:Depends},
|
${misc:Depends},
|
||||||
${shlibs:Depends},
|
${shlibs:Depends},
|
||||||
|
${cargo:Depends}
|
||||||
|
Recommends:
|
||||||
|
${cargo:Recommends}
|
||||||
|
Suggests:
|
||||||
|
${cargo:Suggests}
|
||||||
|
Provides:
|
||||||
|
${cargo:Provides}
|
||||||
|
Built-Using: ${cargo:Built-Using}
|
||||||
|
Static-Built-Using: ${cargo:Static-Built-Using}
|
||||||
Description: CLI tools for interacting with the Gitea API.
|
Description: CLI tools for interacting with the Gitea API.
|
||||||
Use interactively to talk to your Gitea instance, or automatically via a CI/CD
|
Use interactively to talk to your Gitea instance, or automatically via a CI/CD
|
||||||
pipeline. Currently supports:
|
pipeline. Currently supports:
|
||||||
|
|||||||
17
debian/copyright
vendored
17
debian/copyright
vendored
@@ -1,7 +1,7 @@
|
|||||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||||
Upstream-Name: gt-tools
|
Upstream-Name: gt-tool
|
||||||
Upstream-Contact: Robert Garrett <robertgarrett404@gmail.com>
|
Upstream-Contact: Robert Garrett <robertgarrett404@gmail.com>
|
||||||
Source: https://source.mnt.re/reform/mnt-reform-setup-wizard
|
Source: https://git.gelvin.dev/robert/gt-tool
|
||||||
|
|
||||||
Files: *
|
Files: *
|
||||||
Copyright: 2025 Robert Garrett <robertgarrett404@gmail.com>
|
Copyright: 2025 Robert Garrett <robertgarrett404@gmail.com>
|
||||||
@@ -11,19 +11,6 @@ Files: debian/*
|
|||||||
Copyright: 2025 Robert Garrett <robertgarrett404@gmail.com>
|
Copyright: 2025 Robert Garrett <robertgarrett404@gmail.com>
|
||||||
License: GPL-3+
|
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+
|
License: GPL-3+
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
|
|||||||
2
debian/gbp.conf
vendored
2
debian/gbp.conf
vendored
@@ -2,5 +2,5 @@
|
|||||||
compression = xz
|
compression = xz
|
||||||
compression-level = 9
|
compression-level = 9
|
||||||
upstream-tag = v%(version)s
|
upstream-tag = v%(version)s
|
||||||
debian-branch = deb
|
debian-branch = deb/trixie
|
||||||
|
|
||||||
|
|||||||
@@ -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.64 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 febccc4..cf52754 100644
|
|
||||||
--- a/Cargo.toml
|
|
||||||
+++ b/Cargo.toml
|
|
||||||
@@ -1,7 +1,7 @@
|
|
||||||
[package]
|
|
||||||
name = "gt-tool"
|
|
||||||
version = "1.0.0"
|
|
||||||
-edition = "2024"
|
|
||||||
+edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
clap = { version = "4.0.7", features = ["derive", "env"] }
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
From: Robert Garrett <robertgarrett404@gmail.com>
|
|
||||||
Date: Sun, 1 Jun 2025 18:01:21 -0500
|
|
||||||
Subject: Use pre Rust 1.81 compatible file-exists test
|
|
||||||
|
|
||||||
The function `std::fs::exists(...)` was stabilized in Rust 1.81, which
|
|
||||||
means it can't be used in the Debian Bookworm build. This patch swaps to
|
|
||||||
a compatible implementation leaning on the std::path::Path struct.
|
|
||||||
---
|
|
||||||
src/api/release_attachment.rs | 5 +++--
|
|
||||||
1 file changed, 3 insertions(+), 2 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/src/api/release_attachment.rs b/src/api/release_attachment.rs
|
|
||||||
index 9af71bf..8a0c798 100644
|
|
||||||
--- a/src/api/release_attachment.rs
|
|
||||||
+++ b/src/api/release_attachment.rs
|
|
||||||
@@ -1,4 +1,4 @@
|
|
||||||
-use std::fs;
|
|
||||||
+use std::{fs, path};
|
|
||||||
|
|
||||||
pub fn check_release_match_repo() {}
|
|
||||||
pub fn get_release_attachment() {}
|
|
||||||
@@ -16,7 +16,8 @@ pub async fn create_release_attachment(
|
|
||||||
|
|
||||||
// Ensure all files exists before starting the uploads
|
|
||||||
for file in &files {
|
|
||||||
- match fs::exists(file) {
|
|
||||||
+ let path = path::Path::new(file);
|
|
||||||
+ match path.try_exists() {
|
|
||||||
Ok(true) => continue,
|
|
||||||
Ok(false) => return Err(crate::Error::NoSuchFile),
|
|
||||||
Err(e) => {
|
|
||||||
2
debian/patches/series
vendored
2
debian/patches/series
vendored
@@ -1,2 +0,0 @@
|
|||||||
0001-Rust-edition-downgrade-to-2021.patch
|
|
||||||
0002-Use-pre-Rust-1.81-compatible-file-exists-test.patch
|
|
||||||
25
debian/rules
vendored
25
debian/rules
vendored
@@ -1,26 +1,7 @@
|
|||||||
#!/usr/bin/make -f
|
#!/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
|
dh $@ --buildsystem cargo
|
||||||
|
|
||||||
execute_after_dh_auto_clean:
|
override_dh_auto_test:
|
||||||
$(CARGO) clean
|
dh_auto_test -- test --all
|
||||||
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
|
|
||||||
|
|||||||
14
debian/tests/control
vendored
Normal file
14
debian/tests/control
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Test-Command: /usr/share/cargo/bin/cargo-auto-test gt-tool 3.0.1 --all-targets --all-features
|
||||||
|
Features: test-name=rust-gt-tool:@
|
||||||
|
Depends: dh-cargo (>= 31), rustc, @
|
||||||
|
Restrictions: allow-stderr, skip-not-installable
|
||||||
|
|
||||||
|
Test-Command: /usr/share/cargo/bin/cargo-auto-test gt-tool 3.0.1 --all-targets
|
||||||
|
Features: test-name=librust-gt-tool-dev:default
|
||||||
|
Depends: dh-cargo (>= 31), rustc, @
|
||||||
|
Restrictions: allow-stderr, skip-not-installable
|
||||||
|
|
||||||
|
Test-Command: /usr/share/cargo/bin/cargo-auto-test gt-tool 3.0.1 --all-targets --no-default-features
|
||||||
|
Features: test-name=librust-gt-tool-dev:
|
||||||
|
Depends: dh-cargo (>= 31), rustc, @
|
||||||
|
Restrictions: allow-stderr, skip-not-installable
|
||||||
@@ -1,14 +1,9 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ApiError, Result,
|
Result,
|
||||||
structs::{
|
structs::release::{CreateReleaseOption, Release},
|
||||||
self,
|
|
||||||
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> {
|
||||||
@@ -22,24 +17,22 @@ 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)?;
|
||||||
let release_list = response
|
if response.status().is_success() {
|
||||||
.json::<Vec<Release>>()
|
let release_list = response
|
||||||
.await
|
.json::<Vec<Release>>()
|
||||||
.map_err(|reqwest_err| {
|
.await
|
||||||
// Convert reqwest errors to my own
|
.map_err(|reqwest_err| {
|
||||||
// TODO: Create all error variants (see lib.rs)
|
// Convert reqwest errors to my own
|
||||||
crate::Error::WrappedReqwestErr(reqwest_err)
|
// TODO: Create all error variants (see lib.rs)
|
||||||
})?;
|
crate::Error::WrappedReqwestErr(reqwest_err)
|
||||||
return Ok(release_list);
|
})?;
|
||||||
}
|
return Ok(release_list);
|
||||||
|
} else if response.status().is_client_error() {
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
let mesg = crate::decode_client_error(response).await?;
|
||||||
#[serde(untagged)]
|
return Err(crate::Error::ApiErrorMessage(mesg));
|
||||||
enum CreateResult {
|
}
|
||||||
Success(structs::release::Release),
|
panic!("Reached end of list_releases without matching a return pathway.");
|
||||||
ErrWithMessage(ApiError),
|
|
||||||
Empty,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_release(
|
pub async fn create_release(
|
||||||
@@ -49,31 +42,27 @@ pub async fn create_release(
|
|||||||
submission: CreateReleaseOption,
|
submission: CreateReleaseOption,
|
||||||
) -> Result<Release> {
|
) -> Result<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
|
let response = client
|
||||||
.post(request_url)
|
.post(request_url)
|
||||||
.json(&submission)
|
.json(&submission)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::Error::from(e))?;
|
.map_err(crate::Error::from)?;
|
||||||
let new_release = req
|
if response.status().is_success() {
|
||||||
.json::<CreateResult>()
|
let new_release = response
|
||||||
.await
|
.json::<Release>()
|
||||||
.map_err(|e| crate::Error::from(e))?;
|
.await
|
||||||
match new_release {
|
.map_err(crate::Error::from)?;
|
||||||
CreateResult::Success(release) => Ok(release),
|
return Ok(new_release);
|
||||||
CreateResult::ErrWithMessage(api_error) => {
|
} else if response.status().is_client_error() {
|
||||||
if api_error.message == "token is required" {
|
let mesg = crate::decode_client_error(response).await?;
|
||||||
Err(crate::Error::MissingAuthToken)
|
return Err(crate::Error::ApiErrorMessage(mesg));
|
||||||
} else {
|
|
||||||
Err(crate::Error::ApiErrorMessage(api_error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CreateResult::Empty => panic!("How can we have 200 OK and no release info? No. Crash"),
|
|
||||||
}
|
}
|
||||||
|
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!();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use std::fs;
|
use std::{fs, path};
|
||||||
|
|
||||||
|
use crate::structs::Attachment;
|
||||||
|
|
||||||
pub fn check_release_match_repo() {}
|
pub fn check_release_match_repo() {}
|
||||||
pub fn get_release_attachment() {}
|
pub fn get_release_attachment() {}
|
||||||
@@ -10,174 +12,47 @@ pub async fn create_release_attachment(
|
|||||||
gitea_url: &str,
|
gitea_url: &str,
|
||||||
repo: &str,
|
repo: &str,
|
||||||
release_id: usize,
|
release_id: usize,
|
||||||
files: Vec<String>,
|
file: String,
|
||||||
) -> crate::Result<()> {
|
) -> crate::Result<Attachment> {
|
||||||
let request_url = format!("{gitea_url}/api/v1/repos/{repo}/releases/{release_id}/assets");
|
let request_url = format!("{gitea_url}/api/v1/repos/{repo}/releases/{release_id}/assets");
|
||||||
|
|
||||||
// Ensure all files exists before starting the uploads
|
let path = path::Path::new(&file);
|
||||||
for file in &files {
|
match path.try_exists() {
|
||||||
match fs::exists(file) {
|
Ok(true) => (),
|
||||||
Ok(true) => continue,
|
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!(
|
||||||
panic!("TODO: Deal with scenario where the file's existence cannot be checked (e.g.: no permission)");
|
"TODO: Deal with scenario where the file's existence cannot be checked (e.g.: no permission)"
|
||||||
},
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for file in files {
|
println!("Uploading file {}", &file);
|
||||||
println!("Uploading file {}", &file);
|
let data = reqwest::multipart::Part::stream(fs::read(&file).unwrap())
|
||||||
let data = reqwest::multipart::Part::stream(fs::read(&file).unwrap())
|
.file_name("attachment")
|
||||||
.file_name("attachment")
|
.mime_str("text/plain")?;
|
||||||
.mime_str("text/plain")?;
|
|
||||||
|
|
||||||
let form = reqwest::multipart::Form::new().part("attachment", data);
|
let form = reqwest::multipart::Form::new().part("attachment", data);
|
||||||
|
|
||||||
let request = client
|
let response = client
|
||||||
.post(&request_url)
|
.post(&request_url)
|
||||||
.multipart(form)
|
.multipart(form)
|
||||||
.query(&[("name", file.split("/").last())])
|
.query(&[("name", file.split("/").last())])
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
if response.status().is_success() {
|
||||||
|
// TODO: create a struct Attachment and return it to the caller.
|
||||||
|
let attachment_desc = response
|
||||||
|
.json::<Attachment>()
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
return Ok(attachment_desc);
|
||||||
|
} else if response.status().is_client_error() {
|
||||||
|
let mesg = crate::decode_client_error(response).await?;
|
||||||
|
return Err(crate::Error::ApiErrorMessage(mesg));
|
||||||
}
|
}
|
||||||
Ok(())
|
panic!("Reached end of release_attachment without matching a return path");
|
||||||
}
|
}
|
||||||
pub fn edit_release_attachment() {}
|
pub fn edit_release_attachment() {}
|
||||||
pub fn delete_release_attachment() {}
|
pub fn delete_release_attachment() {}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use reqwest::header::{self, ACCEPT};
|
|
||||||
|
|
||||||
use crate::structs::release::Release;
|
|
||||||
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn attach_file_exists() {
|
|
||||||
let conf = TestConfig::new();
|
|
||||||
let release_candidates =
|
|
||||||
crate::api::release::list_releases(
|
|
||||||
&conf.client,
|
|
||||||
&conf.server,
|
|
||||||
&conf.repo
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("Failed to get releases. Pre-conditions unmet, aborting test!");
|
|
||||||
|
|
||||||
let release = match_release_by_tag(&conf.release_tag, release_candidates)
|
|
||||||
.expect("Failed to select matching release. Pre-conditions unmet, aborting test!");
|
|
||||||
|
|
||||||
let api_result = super::create_release_attachment(
|
|
||||||
&conf.client,
|
|
||||||
&conf.server,
|
|
||||||
&conf.repo,
|
|
||||||
release.id,
|
|
||||||
vec![String::from("Cargo.toml")],
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn attach_file_missing() {
|
|
||||||
let conf = TestConfig::new();
|
|
||||||
let release_candidates =
|
|
||||||
crate::api::release::list_releases(
|
|
||||||
&conf.client,
|
|
||||||
&conf.server,
|
|
||||||
&conf.repo
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("Failed to get releases. Pre-conditions unmet, aborting test!");
|
|
||||||
|
|
||||||
let release = match_release_by_tag(&conf.release_tag, release_candidates)
|
|
||||||
.expect("Failed to select matching release. Pre-conditions unmet, aborting test!");
|
|
||||||
|
|
||||||
let api_result = super::create_release_attachment(
|
|
||||||
&conf.client,
|
|
||||||
&conf.server,
|
|
||||||
&conf.repo,
|
|
||||||
release.id,
|
|
||||||
vec![String::from("./this-file-doesnt-exist")],
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
let api_err = api_result.expect_err("Received Ok(()) after uploading non-existent file. That's nonsense, the API Function is wrong.");
|
|
||||||
match api_err {
|
|
||||||
crate::Error::Placeholder => panic!("Received dummy response from the API function. Finish implementing it, stupid"),
|
|
||||||
crate::Error::WrappedReqwestErr(error) => panic!("Received a reqwest::Error from the API function: {error}"),
|
|
||||||
crate::Error::MissingAuthToken => unreachable!("Missing auth token... in a unit test that already panics without the auth token..."),
|
|
||||||
crate::Error::NoSuchFile => (), // test passes
|
|
||||||
crate::Error::ApiErrorMessage(api_error) => panic!("Received an error message from the API: {api_error:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TestConfig {
|
|
||||||
server: String,
|
|
||||||
repo: String,
|
|
||||||
release_tag: String,
|
|
||||||
client: reqwest::Client,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestConfig {
|
|
||||||
fn new() -> Self {
|
|
||||||
let server = std::env::var("TEST_GITEA_SERVER")
|
|
||||||
.expect("Must set server address in env var \"TEST_GITEA_SERVER\"");
|
|
||||||
let repo = std::env::var("TEST_GITEA_REPO")
|
|
||||||
.expect("Must set <user>/<repo> name in env var \"TEST_GITEA_REPO\"");
|
|
||||||
let token = format!(
|
|
||||||
"token {}",
|
|
||||||
std::env::var("TEST_GITEA_KEY")
|
|
||||||
.expect("Must set the API token in env var \"TEST_GITEA_KEY\"")
|
|
||||||
);
|
|
||||||
let release_tag = std::env::var("TEST_GITEA_RELEASE_TAG")
|
|
||||||
.expect("Must set the target release tag in env var \"TEST_GITEA_RELEASE_TAG\"");
|
|
||||||
|
|
||||||
let mut headers = reqwest::header::HeaderMap::new();
|
|
||||||
headers.append(ACCEPT, header::HeaderValue::from_static("application/json"));
|
|
||||||
headers.append("Authorization", token.parse().unwrap());
|
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
|
||||||
.user_agent(format!(
|
|
||||||
"gt-tools-autotest-agent{}",
|
|
||||||
env!("CARGO_PKG_VERSION")
|
|
||||||
))
|
|
||||||
.default_headers(headers)
|
|
||||||
.build()
|
|
||||||
.expect("Failed to build reqwest::Client.");
|
|
||||||
|
|
||||||
return Self {
|
|
||||||
server,
|
|
||||||
repo,
|
|
||||||
release_tag,
|
|
||||||
client
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Testing utils
|
|
||||||
fn match_release_by_tag(tag: &String, releases: Vec<Release>) -> Option<Release> {
|
|
||||||
let mut release: Option<Release> = None;
|
|
||||||
for rel in releases {
|
|
||||||
if rel.tag_name == *tag {
|
|
||||||
// Only store the value if one hasn't been stored already
|
|
||||||
if let Some(first_release) = &release {
|
|
||||||
// if there was already a match, begin the error diagnostic creation.
|
|
||||||
let first_id = first_release.id;
|
|
||||||
let second_id = rel.id;
|
|
||||||
assert!(
|
|
||||||
first_id != second_id,
|
|
||||||
"FAILURE: Found the same release ID twice while scanning for duplicate tags. How did we get the same one twice?"
|
|
||||||
);
|
|
||||||
eprintln!("ERROR: Two releases have been found for the tag \"{tag}\".");
|
|
||||||
eprintln!("ERROR: first ID: {first_id}");
|
|
||||||
eprintln!("ERROR: second ID: {second_id}");
|
|
||||||
panic!("ERROR: Nonsense detected, I'm bailing out!");
|
|
||||||
} else {
|
|
||||||
// else, store our first (and hopefully only) match
|
|
||||||
release = Some(rel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return release;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14
src/cli.rs
14
src/cli.rs
@@ -4,9 +4,17 @@ use clap::{Parser, Subcommand};
|
|||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
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: String,
|
pub gitea_url: Option<String>,
|
||||||
#[arg(short = 'r', long = "repo", env = "GTTOOL_FQRN")]
|
#[arg(short = 'o', long = "owner", env = "GTTOOL_OWNER")]
|
||||||
pub repo: String,
|
pub owner: Option<String>,
|
||||||
|
#[arg(short = 'r', long = "repo", env = "GTTOOL_REPO")]
|
||||||
|
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,
|
||||||
|
|||||||
352
src/config.rs
Normal file
352
src/config.rs
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
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,
|
||||||
|
CouldntReadFile,
|
||||||
|
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 {}
|
||||||
|
|
||||||
|
/// Creates an iterator of default (Linux) search paths. The iterator output
|
||||||
|
/// is a list of files named "gt-tool.toml" found in decreasingly specific
|
||||||
|
/// configuration folders.
|
||||||
|
///
|
||||||
|
/// - any dirs listed in env var `$XDG_CONFIG_DIRS`
|
||||||
|
/// - and the `/etc` dir
|
||||||
|
///
|
||||||
|
/// This is so that user-specific configs are used first, then machine-wide
|
||||||
|
/// ones.
|
||||||
|
pub fn default_paths() -> impl Iterator<Item = PathBuf> {
|
||||||
|
// Read env var `XDG_CONFIG_DIRS` and split on ":" to get highest-priority list
|
||||||
|
// TODO: Emit warning when paths aren't unicode
|
||||||
|
std::env::var("XDG_CONFIG_DIRS")
|
||||||
|
.unwrap_or(String::from(""))
|
||||||
|
.split(":")
|
||||||
|
// Set up the "/etc" list
|
||||||
|
// Which is pretty silly, in this case.
|
||||||
|
// Maybe a future version will scan nested folders and this will make
|
||||||
|
// more sense.
|
||||||
|
// glue on the "/etc" path
|
||||||
|
.chain(["/etc"])
|
||||||
|
.map(|path_str| {
|
||||||
|
let mut path = PathBuf::from(path_str);
|
||||||
|
path.push("gt-tool.toml");
|
||||||
|
path
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Searches through the files, `search_files`, for configuration related to a
|
||||||
|
/// project, `project`.
|
||||||
|
///
|
||||||
|
/// The project string is used as a map key and should match the real path of a
|
||||||
|
/// project on disk. These are the table names for each config section.
|
||||||
|
///
|
||||||
|
/// The search files iterator must produce *files* not *folders.* For now,
|
||||||
|
/// there is no mechanism to ensure correct usage. Files that can't be opened
|
||||||
|
/// will quietly be skipped, so there will be no warning when one gives a
|
||||||
|
/// folder.
|
||||||
|
///
|
||||||
|
/// Use `fn default_paths()` to get a reasonable (Linux) default.
|
||||||
|
///
|
||||||
|
/// TODO: Check for, and warn or error when given a dir.
|
||||||
|
pub fn get_config(
|
||||||
|
project: &str,
|
||||||
|
search_files: impl Iterator<Item = PathBuf>,
|
||||||
|
) -> Result<PartialConfig> {
|
||||||
|
/*
|
||||||
|
1. Get conf search (from fn input)
|
||||||
|
2. Iterate config dirs
|
||||||
|
3. Try load toml::Value from file
|
||||||
|
4. Try-get proj-specific table
|
||||||
|
5. Try-get "[all]" table
|
||||||
|
6. (merge) Update `Option::None`s in proj-spec with `Some(_)`s from "[all]"
|
||||||
|
7. (merge, again) Fold the PartialConfigs into a finished one
|
||||||
|
*/
|
||||||
|
|
||||||
|
let file_iter = search_files;
|
||||||
|
|
||||||
|
let toml_iter = file_iter
|
||||||
|
.map(std::fs::read_to_string) // read text from file
|
||||||
|
.filter_map(|res| res.ok()) // remove any error messages
|
||||||
|
// TODO: Log warnings when files couldn't be read.
|
||||||
|
.map(|toml_text| toml_text.parse::<Value>()) // try convert to `toml::Value`
|
||||||
|
.filter_map(|res| res.ok()); // remove any failed parses
|
||||||
|
let config_iter = toml_iter
|
||||||
|
.map(|val| -> Result<PartialConfig> {
|
||||||
|
// Like `fn read_conf_str(...)`, but doesn't produce a `WholeFile`
|
||||||
|
|
||||||
|
// 1. Get the top-level table that is the config file
|
||||||
|
let cfg_table = val.as_table().ok_or(Error::BadFormat)?;
|
||||||
|
|
||||||
|
// 2. Get table
|
||||||
|
|
||||||
|
get_table(cfg_table, project)
|
||||||
|
// 3a. convert to PartialConfig
|
||||||
|
.and_then(PartialConfig::try_from)
|
||||||
|
// 3b. or default, if the table couldn't be found.
|
||||||
|
.or(Ok(PartialConfig::default()))
|
||||||
|
// 4. assemble a 2-tuple of PartialConfigs by...
|
||||||
|
.map(|proj| {
|
||||||
|
(
|
||||||
|
// 4-1. Passing in the project-specific PartialConfig
|
||||||
|
proj.project_path(project),
|
||||||
|
// 4-2. Getting and converting to PartialConfig, or returning any Err() if one appears.
|
||||||
|
get_table(cfg_table, "all")
|
||||||
|
.and_then(PartialConfig::try_from)
|
||||||
|
.unwrap_or(PartialConfig::default()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|pair| pair.0.merge(pair.1))
|
||||||
|
})
|
||||||
|
.filter_map(|res| res.ok())
|
||||||
|
.fold(PartialConfig::default(), |acc, inc| acc.merge(inc));
|
||||||
|
Ok(config_iter)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
|
pub struct PartialConfig {
|
||||||
|
project_path: Option<String>,
|
||||||
|
pub gitea_url: Option<String>,
|
||||||
|
pub owner: Option<String>,
|
||||||
|
pub repo: Option<String>,
|
||||||
|
pub 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merges two `PartialConfig`'s together, producing a new one.
|
||||||
|
// Non-None values on the right-hand side are used to replace values
|
||||||
|
// in the left-hand side, even if they are Some(_).
|
||||||
|
fn merge(self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
project_path: other.project_path.or(self.project_path),
|
||||||
|
gitea_url: other.gitea_url.or(self.gitea_url),
|
||||||
|
owner: other.owner.or(self.owner),
|
||||||
|
repo: other.repo.or(self.repo),
|
||||||
|
token: other.token.or(self.token),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&Table> for PartialConfig {
|
||||||
|
type Error = crate::config::Error;
|
||||||
|
|
||||||
|
/// Scans properties out of a `toml::Table` to get a PartialConfig.
|
||||||
|
///
|
||||||
|
/// `Error::NoSuchProperty` is quietly ignored (mapped to `None`) since it
|
||||||
|
/// isn't an error in this context.
|
||||||
|
///
|
||||||
|
/// All other errors are propagated and should be treated as real failures.
|
||||||
|
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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The outer value must be a Table so we can get the sub-table from it.
|
||||||
|
fn get_table(outer: &Table, table_name: impl ToString) -> Result<&Table> {
|
||||||
|
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: &Table, property: impl ToString) -> Result<Option<&String>> {
|
||||||
|
let maybe_prop = get_property(outer, property);
|
||||||
|
match maybe_prop {
|
||||||
|
Ok(value) => Ok(Some(value)),
|
||||||
|
Err(e) => {
|
||||||
|
if let Error::NoSuchProperty = e {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
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: &Table, property: impl ToString) -> Result<&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 test_get_config_with_specific_match() -> Result<()> {
|
||||||
|
let search_paths = ["./test_data/sample_config.toml"]
|
||||||
|
.into_iter()
|
||||||
|
.map(PathBuf::from);
|
||||||
|
let load_result = get_config("/home/robert/projects/gt-tool", search_paths)?;
|
||||||
|
let expected = PartialConfig {
|
||||||
|
project_path: Some(String::from("/home/robert/projects/gt-tool")),
|
||||||
|
owner: Some(String::from("robert")),
|
||||||
|
repo: Some(String::from("gt-tool")),
|
||||||
|
gitea_url: Some(String::from("http://localhost:3000")),
|
||||||
|
token: Some(String::from("fake-token")),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(load_result, expected);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the config comes back with something even when there is no
|
||||||
|
// matching project-specific table.
|
||||||
|
#[test]
|
||||||
|
fn test_get_config_no_specific_match() -> Result<()> {
|
||||||
|
let search_paths = ["./test_data/sample_config.toml"]
|
||||||
|
.into_iter()
|
||||||
|
.map(PathBuf::from);
|
||||||
|
let load_result = get_config("/no/such/project", search_paths)?;
|
||||||
|
let expected = PartialConfig {
|
||||||
|
project_path: Some(String::from("/no/such/project")),
|
||||||
|
owner: None,
|
||||||
|
repo: None,
|
||||||
|
gitea_url: Some(String::from("http://localhost:3000")),
|
||||||
|
token: Some(String::from("fake-token")),
|
||||||
|
};
|
||||||
|
assert_eq!(load_result, expected);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the config comes back with something even when there is no
|
||||||
|
// "[all]" table
|
||||||
|
#[test]
|
||||||
|
fn test_get_config_without_all() -> Result<()> {
|
||||||
|
let search_paths = ["./test_data/missing_all_table.toml"]
|
||||||
|
.into_iter()
|
||||||
|
.map(PathBuf::from);
|
||||||
|
let load_result = get_config("/some/other/path", search_paths)?;
|
||||||
|
let expected = PartialConfig {
|
||||||
|
project_path: Some(String::from("/some/other/path")),
|
||||||
|
gitea_url: Some(String::from("fake-url")),
|
||||||
|
..PartialConfig::default()
|
||||||
|
};
|
||||||
|
assert_eq!(load_result, expected);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that trying to load files that don't exist simply get skipped over
|
||||||
|
// instead of causing a short-circuit exit or other bogus output.
|
||||||
|
#[test]
|
||||||
|
fn test_get_config_many_missing_files() -> Result<()> {
|
||||||
|
let search_paths = [
|
||||||
|
"./test_data/not_real_1.toml",
|
||||||
|
"./test_data/not_real_2.toml",
|
||||||
|
"./test_data/not_real_3.toml",
|
||||||
|
"./test_data/not_real_4.toml",
|
||||||
|
"./test_data/not_real_5.toml",
|
||||||
|
"./test_data/sample_config.toml",
|
||||||
|
"./test_data/not_real_6.toml",
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(PathBuf::from);
|
||||||
|
let load_result = get_config("/home/robert/projects/gt-tool", search_paths)?;
|
||||||
|
let expected = PartialConfig {
|
||||||
|
project_path: Some(String::from("/home/robert/projects/gt-tool")),
|
||||||
|
owner: Some(String::from("robert")),
|
||||||
|
repo: Some(String::from("gt-tool")),
|
||||||
|
gitea_url: Some(String::from("http://localhost:3000")),
|
||||||
|
token: Some(String::from("fake-token")),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(load_result, expected);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/lib.rs
21
src/lib.rs
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
|
pub mod config;
|
||||||
pub mod structs;
|
pub mod structs;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
@@ -10,9 +11,21 @@ pub struct ApiError {
|
|||||||
url: String,
|
url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn decode_client_error(response: reqwest::Response) -> Result<ApiError> {
|
||||||
|
response
|
||||||
|
.json::<ApiError>()
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::WrappedReqwestErr)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
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.
|
||||||
|
MissingRepoFRQN, // either the owner, repo, or both weren't specified in the loaded PartialConfig
|
||||||
|
MissingRepoOwner,
|
||||||
|
MissingRepoName,
|
||||||
|
WrappedConfigErr(config::Error),
|
||||||
WrappedReqwestErr(reqwest::Error),
|
WrappedReqwestErr(reqwest::Error),
|
||||||
MissingAuthToken,
|
MissingAuthToken,
|
||||||
NoSuchFile, // for release attachment 'file exists' pre-check.
|
NoSuchFile, // for release attachment 'file exists' pre-check.
|
||||||
@@ -26,4 +39,10 @@ impl From<reqwest::Error> for crate::Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<crate::config::Error> for crate::Error {
|
||||||
|
fn from(value: crate::config::Error) -> Self {
|
||||||
|
Self::WrappedConfigErr(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, Error>;
|
type Result<T> = core::result::Result<T, Error>;
|
||||||
|
|||||||
100
src/main.rs
100
src/main.rs
@@ -1,3 +1,5 @@
|
|||||||
|
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};
|
||||||
|
|
||||||
@@ -10,6 +12,38 @@ 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();
|
||||||
|
|
||||||
|
let project_path =
|
||||||
|
args.project
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.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(
|
||||||
|
project_path
|
||||||
|
.to_str()
|
||||||
|
.expect("I assumed the path can be UTF-8, but that didn't work out..."),
|
||||||
|
gt_tool::config::default_paths(),
|
||||||
|
)?;
|
||||||
|
println!("->> Loaded Config: {config:?}");
|
||||||
|
// arg parser also checks the environment. Prefer CLI/env, then config file.
|
||||||
|
let gitea_url = args
|
||||||
|
.gitea_url
|
||||||
|
.or(config.gitea_url)
|
||||||
|
.ok_or(gt_tool::Error::MissingGiteaUrl)?;
|
||||||
|
|
||||||
|
let owner = args
|
||||||
|
.owner
|
||||||
|
.or(config.owner)
|
||||||
|
.ok_or(gt_tool::Error::MissingRepoOwner)?;
|
||||||
|
|
||||||
|
let repo = args
|
||||||
|
.repo
|
||||||
|
.or(config.repo)
|
||||||
|
.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"));
|
||||||
|
|
||||||
@@ -19,20 +53,23 @@ 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()?;
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
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, &gitea_url, &repo_fqrn).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,
|
||||||
@@ -49,13 +86,8 @@ 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, &gitea_url, &repo_fqrn, submission)
|
||||||
&client,
|
.await?;
|
||||||
&args.gitea_url,
|
|
||||||
&args.repo,
|
|
||||||
submission,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
gt_tool::cli::Commands::UploadRelease {
|
gt_tool::cli::Commands::UploadRelease {
|
||||||
tag_name,
|
tag_name,
|
||||||
@@ -75,17 +107,28 @@ async fn main() -> Result<(), gt_tool::Error> {
|
|||||||
// Grab all, find the one that matches the input tag.
|
// Grab all, find the one that matches the input tag.
|
||||||
// Scream if there are multiple matches.
|
// Scream if there are multiple matches.
|
||||||
let release_candidates =
|
let release_candidates =
|
||||||
gt_tool::api::release::list_releases(&client, &args.gitea_url, &args.repo).await?;
|
gt_tool::api::release::list_releases(&client, &gitea_url, &repo_fqrn).await?;
|
||||||
|
|
||||||
if let Some(release) = match_release_by_tag(&tag_name, release_candidates) {
|
if let Some(release) = match_release_by_tag(&tag_name, release_candidates) {
|
||||||
gt_tool::api::release_attachment::create_release_attachment(
|
for file in &files {
|
||||||
&client,
|
let path = path::Path::new(&file);
|
||||||
&args.gitea_url,
|
match path.try_exists() {
|
||||||
&args.repo,
|
Ok(true) => continue,
|
||||||
release.id,
|
Ok(false) => return Err(gt_tool::Error::NoSuchFile),
|
||||||
files,
|
Err(e) => {
|
||||||
)
|
eprintln!("Uh oh! The file-exists check couldn't be done: {e}");
|
||||||
.await?;
|
panic!(
|
||||||
|
"TODO: Deal with scenario where the file's existence cannot be checked (e.g.: no permission)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for file in files {
|
||||||
|
let _attach_desc = gt_tool::api::release_attachment::create_release_attachment(
|
||||||
|
&client, &gitea_url, &repo_fqrn, release.id, file,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("ERR: Couldn't find a release matching the tag \"{tag_name}\".");
|
println!("ERR: Couldn't find a release matching the tag \"{tag_name}\".");
|
||||||
return Err(gt_tool::Error::NoSuchRelease);
|
return Err(gt_tool::Error::NoSuchRelease);
|
||||||
@@ -127,5 +170,12 @@ fn match_release_by_tag(tag: &String, releases: Vec<Release>) -> Option<Release>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,15 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub mod release;
|
pub mod release;
|
||||||
pub mod repo;
|
pub mod repo;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Attachment {
|
||||||
|
id: usize,
|
||||||
|
name: String,
|
||||||
|
size: i64,
|
||||||
|
download_count: i64,
|
||||||
|
created_at: String, // TODO: Date-time struct
|
||||||
|
uuid: String,
|
||||||
|
browser_download_url: String,
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
5
test_data/missing_all_table.toml
Normal file
5
test_data/missing_all_table.toml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# There must be an "[all]" table or the loader will reject the config file.
|
||||||
|
|
||||||
|
["/some/other/path"]
|
||||||
|
gitea_url = "fake-url"
|
||||||
|
|
||||||
15
test_data/sample_config.toml
Normal file
15
test_data/sample_config.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[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"
|
||||||
Reference in New Issue
Block a user