Compare commits
20 Commits
333636b524
...
feature/pa
| Author | SHA1 | Date | |
|---|---|---|---|
| 024646acf4 | |||
| ab8b21d01c | |||
| c6dd63143e | |||
| 94ec8cfd71 | |||
| 55b992b5c4 | |||
| 7b02063bb7 | |||
| 59f67e1d4e | |||
| d982f42ae7 | |||
| 641efc3bf7 | |||
| 144fba5373 | |||
| 7f35b808e5 | |||
| 00edaf87ce | |||
| 250140a954 | |||
| b290a8b1d6 | |||
| b952e40060 | |||
| 4b9257a9a7 | |||
| d34eda77dc | |||
| 3315c18ed2 | |||
| 0e7bca80cb | |||
| 5bd2862498 |
@@ -22,7 +22,9 @@ jobs:
|
||||
- name: Upload the program (using itself!)
|
||||
run: >
|
||||
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
|
||||
"${{ github.ref_name }}"
|
||||
target/release/gt-tool-${{ github.ref_name }}-$(arch)
|
||||
|
||||
16
Cargo.toml
16
Cargo.toml
@@ -1,16 +1,16 @@
|
||||
[package]
|
||||
name = "gt-tool"
|
||||
version = "3.0.0-alpha.1"
|
||||
version = "4.0.0-dev"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.0.7", features = ["derive", "env"] }
|
||||
colored = "2.0.0"
|
||||
itertools = "0.10.0"
|
||||
reqwest = { version = "0.11.13", features = ["json", "stream", "multipart"] }
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] }
|
||||
toml = "0.5"
|
||||
clap = { version = "4.5.23", features = ["derive", "env"] }
|
||||
colored = "2.2.0"
|
||||
itertools = "0.13.0"
|
||||
reqwest = { version = "0.12.15", features = ["json", "stream", "multipart"] }
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
tokio = { version = "1.43.1", features = ["macros", "rt-multi-thread"] }
|
||||
toml = "0.8.19"
|
||||
|
||||
# Packages available in Debian (Sid)
|
||||
# clap = "4.5.23"
|
||||
|
||||
70
README.md
70
README.md
@@ -15,41 +15,51 @@ Commands:
|
||||
|
||||
Options:
|
||||
-u, --url <GITEA_URL> [env: GTTOOL_GITEA_URL=]
|
||||
-r, --repo <REPO> [env: GTTOOL_FQRN=]
|
||||
-p, --project <PROJECT> Path to project (relative or absolute). Used to select configuration.
|
||||
-o, --owner <OWNER> [env: GTTOOL_OWNER=]
|
||||
-r, --repo <REPO> [env: GTTOOL_REPO=]
|
||||
-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 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>`:
|
||||
|
||||
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.
|
||||
|
||||
E.g.: `--repo "go-gitea/gitea"` would name the Gitea repo belonging to the go-gitea organization.
|
||||
|
||||
### `<PROJECT>`
|
||||
|
||||
Override the default (current-directory) project name when searching through the config files for this project's settings.
|
||||
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.
|
||||
|
||||
See [configuration](#configuration) for details on format and file locations.
|
||||
|
||||
### `<COMMAND>`:
|
||||
|
||||
One of these, defaults to `help`:
|
||||
### Commands:
|
||||
|
||||
| Command | Description |
|
||||
|-|-|
|
||||
@@ -64,6 +74,8 @@ Instead of specifying everything on the command line every single run, some TOML
|
||||
|
||||
> 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
|
||||
@@ -74,7 +86,7 @@ repo = "gt-tool"
|
||||
token = "fake-token"
|
||||
```
|
||||
|
||||
Some may apply to all projects. For this, one can use the special `[all]` table.
|
||||
Sometimes one may want to apply a setting to all projects. For this, they can use the special `[all]` table.
|
||||
|
||||
```toml
|
||||
[all]
|
||||
@@ -87,7 +99,6 @@ Since the more-specific settings are preferred, these can be combined to have an
|
||||
[all]
|
||||
gitea_url = "https://demo.gitea.com/"
|
||||
owner = "robert"
|
||||
# no `repo = ` section because that must be project specific.
|
||||
token = "fake-token"
|
||||
|
||||
# Override Gitea target so I can test my uploads privately.
|
||||
@@ -96,7 +107,12 @@ gitea_url = "http://localhost:3000"
|
||||
repo = "gt-tool"
|
||||
```
|
||||
|
||||
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. First, each dir in `$XDG_CONFIG_DIRS` is scanned for a `gt-tool.toml` file. Then, `/etc/gt-tool.toml`.
|
||||
### 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`.
|
||||
|
||||
@@ -110,3 +126,11 @@ Similar to how unspecified project settings will fall back to those in the "`[al
|
||||
| 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
|
||||
|
||||
# Errata
|
||||
|
||||
- Gitea's package *uploading* API endpoints are not documented with Swagger.
|
||||
* https://github.com/go-gitea/gitea/issues/30597
|
||||
* It seems this is intentional because of which side defines the protocol (the consumer, not Gitea).
|
||||
* This is an open issue (as of 12-Sep-2025) that doesn't have much attention.
|
||||
* HTTP-POST (`curl`) is sufficient for upload. Use that either directly or as reference to make custom code.
|
||||
|
||||
@@ -1,4 +1,42 @@
|
||||
pub fn list_packages() {}
|
||||
use std::{fs, path::Path};
|
||||
|
||||
use crate::structs::package::{Package, PackageType};
|
||||
|
||||
/// Gets all packages of an owner
|
||||
///
|
||||
/// https://github.com/go-gitea/gitea/blob/main/routers/api/v1/packages/package.go#L22
|
||||
///
|
||||
/// Matches the route `GET /packages/{owner}`
|
||||
pub async fn list_packages(
|
||||
client: &reqwest::Client,
|
||||
gitea_url: &str,
|
||||
owner: &str,
|
||||
pkg_type: Option<PackageType>,
|
||||
) -> crate::Result<Vec<Package>> {
|
||||
let request_url = format!("{gitea_url}/api/v1/packages/{owner}");
|
||||
// add the query parameter only when a package type filter has been set.
|
||||
let request_url = if let Some(pkg_type) = pkg_type {
|
||||
let pkg_type = pkg_type.to_string();
|
||||
format!("{request_url}?type={pkg_type}")
|
||||
} else {
|
||||
request_url
|
||||
};
|
||||
let req = client.get(request_url).send().await;
|
||||
let response = req.map_err(crate::Error::WrappedReqwestErr)?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let release_list = response
|
||||
.json::<Vec<Package>>()
|
||||
.await
|
||||
.map_err(crate::Error::WrappedReqwestErr)?;
|
||||
return Ok(release_list);
|
||||
} else if response.status().is_client_error() {
|
||||
let mesg = crate::decode_client_error(response).await?;
|
||||
return Err(crate::Error::ApiErrorMessage(mesg));
|
||||
}
|
||||
panic!("Reached the end of `api::list_packages()` without matching a return pathway.");
|
||||
}
|
||||
|
||||
pub fn get_packages() {}
|
||||
pub fn delete_package() {}
|
||||
pub fn list_package_files() {}
|
||||
@@ -6,3 +44,73 @@ pub fn get_latest_package_version() {}
|
||||
pub fn link_package() {}
|
||||
pub fn unlink_package() {}
|
||||
pub fn search_packages() {}
|
||||
|
||||
|
||||
/// Upload a Debian package and link it to it's source repository
|
||||
fn upload_debian(
|
||||
client: &reqwest::Client,
|
||||
gitea_url: &str,
|
||||
repo: &str,
|
||||
file: &Path,
|
||||
owner: &str,
|
||||
distribution: &str,
|
||||
component: &str,
|
||||
) -> crate::Result<()> {
|
||||
let request_url = format!("{gitea_url}/api/packages/{owner}/debian/pool/{distribution}/{component}/upload");
|
||||
match file.try_exists() {
|
||||
Ok(true) => (),
|
||||
Ok(false) => return Err(crate::Error::NoSuchFile),
|
||||
Err(e) => {
|
||||
eprintln!("Uh oh! The file-exists check couldn't be done: {e}");
|
||||
panic!(
|
||||
"TODO: Deal with scenario where the file's existence cannot be checked (e.g.: no permission)"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let data = reqwest::multipart::Part::stream(fs::read(&file).unwrap())
|
||||
.file_name("deb-pkg")
|
||||
.mime_str("text/plain")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Uploads a package file to the Gitea registry.
|
||||
///
|
||||
/// This route is not documented in Swagger. The reason seems to be that some
|
||||
/// package managers have their own upload protocol (e.g. `docker push`). Gitea
|
||||
/// implements it but does not define it, so it doesn't include the API docs.
|
||||
pub fn upload_package(
|
||||
client: &reqwest::Client,
|
||||
gitea_url: &str,
|
||||
file: String, // TODO: Use a path buffer of some flavor.
|
||||
) {
|
||||
let request_url = format!("{gitea_url}/{}", route_for_upload(PackageType::Debian));
|
||||
}
|
||||
|
||||
fn route_for_upload(kind: PackageType) -> &'static str {
|
||||
match kind {
|
||||
PackageType::Alpine => "https://gitea.example.com/api/packages/{owner}/alpine/{branch}/{repository}",
|
||||
PackageType::Arch => "api/packages/{owner}/arch/{repository}",
|
||||
PackageType::Cargo => todo!(),
|
||||
PackageType::Chef => todo!(),
|
||||
PackageType::Composer => todo!(),
|
||||
PackageType::Conan => todo!(),
|
||||
PackageType::Conda => todo!(),
|
||||
PackageType::Container => todo!(),
|
||||
PackageType::Cran => todo!(),
|
||||
PackageType::Debian => "https://gitea.example.com/api/packages/{owner}/debian/pool/{distribution}/{component}/upload",
|
||||
PackageType::Generic => todo!(),
|
||||
PackageType::Go => todo!(),
|
||||
PackageType::Helm => todo!(),
|
||||
PackageType::Maven => todo!(),
|
||||
PackageType::Npm => todo!(),
|
||||
PackageType::Nuget => todo!(),
|
||||
PackageType::Pub => todo!(),
|
||||
PackageType::PyPi => todo!(),
|
||||
PackageType::Rpm => todo!(),
|
||||
PackageType::RubyGems => todo!(),
|
||||
PackageType::Swift => todo!(),
|
||||
PackageType::Vagrant => todo!(),
|
||||
}
|
||||
}
|
||||
@@ -44,4 +44,5 @@ pub enum Commands {
|
||||
#[arg()]
|
||||
files: Vec<String>,
|
||||
},
|
||||
ListPackages,
|
||||
}
|
||||
|
||||
18
src/main.rs
18
src/main.rs
@@ -31,16 +31,18 @@ async fn main() -> Result<(), gt_tool::Error> {
|
||||
.or(config.gitea_url)
|
||||
.ok_or(gt_tool::Error::MissingGiteaUrl)?;
|
||||
|
||||
let owner = args.owner
|
||||
let owner = args
|
||||
.owner
|
||||
.or(config.owner)
|
||||
.ok_or(gt_tool::Error::MissingRepoOwner)?;
|
||||
|
||||
let repo = args.repo
|
||||
let repo = args
|
||||
.repo
|
||||
.or(config.repo)
|
||||
.or_else(infer_repo)
|
||||
.ok_or(gt_tool::Error::MissingRepoName)?;
|
||||
|
||||
let repo_fqrn = String::from(format!("{owner}/{repo}"));
|
||||
let repo_fqrn = format!("{owner}/{repo}");
|
||||
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.append(ACCEPT, header::HeaderValue::from_static("application/json"));
|
||||
@@ -132,6 +134,16 @@ async fn main() -> Result<(), gt_tool::Error> {
|
||||
return Err(gt_tool::Error::NoSuchRelease);
|
||||
}
|
||||
}
|
||||
gt_tool::cli::Commands::ListPackages => {
|
||||
let packages = gt_tool::api::packages::list_packages(&client, &gitea_url, &owner, None).await?;
|
||||
itertools::Itertools::intersperse(
|
||||
packages
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|pkg| pkg.colorized()),
|
||||
String::from("")
|
||||
).for_each(|package| println!("{package}"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod package;
|
||||
pub mod release;
|
||||
pub mod repo;
|
||||
|
||||
|
||||
94
src/structs/package.rs
Normal file
94
src/structs/package.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use colored::Colorize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::structs::release::Author;
|
||||
|
||||
/// Represents the package as described by JSON
|
||||
///
|
||||
/// https://github.com/go-gitea/gitea/blob/main/modules/structs/package.go
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Package {
|
||||
created_at: String, // TODO: Datetime struct
|
||||
creator: Author,
|
||||
html_url: String,
|
||||
id: u64,
|
||||
name: String,
|
||||
owner: Author,
|
||||
#[serde(skip)]
|
||||
repository: (), // TODO: Create a `struct Repository`
|
||||
#[serde(rename = "type")] // field is "type" in JSON & Go, but that's a
|
||||
pkg_type: PackageType, // keyword in Rust so we need to serde-rename it.
|
||||
version: String,
|
||||
}
|
||||
|
||||
impl Package {
|
||||
pub fn colorized(&self) -> String {
|
||||
let name = "Name:".green().bold();
|
||||
let version = &self.version;
|
||||
let pkg_type = "Type:".green();
|
||||
|
||||
format!(
|
||||
"{name} {} {version}
|
||||
{pkg_type} {}",
|
||||
self.name,
|
||||
self.version,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A marker for the kind of package being handled.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PackageType {
|
||||
Alpine,
|
||||
Arch,
|
||||
Cargo,
|
||||
Chef,
|
||||
Composer,
|
||||
Conan,
|
||||
Conda,
|
||||
Container,
|
||||
Cran,
|
||||
Debian,
|
||||
Generic,
|
||||
Go,
|
||||
Helm,
|
||||
Maven,
|
||||
Npm,
|
||||
Nuget,
|
||||
Pub,
|
||||
PyPi,
|
||||
Rpm,
|
||||
RubyGems,
|
||||
Swift,
|
||||
Vagrant,
|
||||
}
|
||||
|
||||
impl ToString for PackageType {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
PackageType::Alpine => "alpine".into(),
|
||||
PackageType::Arch => "arch".into(),
|
||||
PackageType::Cargo => "cargo".into(),
|
||||
PackageType::Chef => "chef".into(),
|
||||
PackageType::Composer => "composer".into(),
|
||||
PackageType::Conan => "conan".into(),
|
||||
PackageType::Conda => "conda".into(),
|
||||
PackageType::Container => "container".into(),
|
||||
PackageType::Cran => "cran".into(),
|
||||
PackageType::Debian => "debian".into(),
|
||||
PackageType::Generic => "generic".into(),
|
||||
PackageType::Go => "go".into(),
|
||||
PackageType::Helm => "helm".into(),
|
||||
PackageType::Maven => "maven".into(),
|
||||
PackageType::Npm => "npm".into(),
|
||||
PackageType::Nuget => "nuget".into(),
|
||||
PackageType::Pub => "pub".into(),
|
||||
PackageType::PyPi => "pypi".into(),
|
||||
PackageType::Rpm => "rpm".into(),
|
||||
PackageType::RubyGems => "rubygems".into(),
|
||||
PackageType::Swift => "swift".into(),
|
||||
PackageType::Vagrant => "vagrant".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ impl Release {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Rename to "User" to match Gitea's code
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Author {
|
||||
id: usize,
|
||||
|
||||
Reference in New Issue
Block a user