Compare commits

...

56 Commits

Author SHA1 Message Date
Christoph J. Scherr 872338c83f Merge branch 'master' of https://git.cscherr.de/PlexSheep/autocrate
cargo devel CI / cargo CI (push) Successful in 2m28s Details
2024-04-26 14:24:29 +02:00
Christoph J. Scherr 411ded1395 more print error 2024-04-26 14:24:23 +02:00
PlexSheep 97b36966d5 automatic cargo CI changes 2024-04-26 08:16:07 +00:00
Christoph J. Scherr e7ab89f050 better error display
cargo devel CI / cargo CI (push) Successful in 2m23s Details
2024-04-26 10:13:47 +02:00
Christoph J. Scherr ca3d1b7e82 Merge branch 'devel'
cargo devel CI / cargo CI (push) Successful in 2m13s Details
2024-04-26 10:07:30 +02:00
PlexSheep fcd6b6a4d4 automatic cargo CI changes 2024-04-26 08:07:04 +00:00
Christoph J. Scherr 56d6a50142 Merge branch 'devel'
cargo devel CI / cargo CI (push) Has been cancelled Details
2024-04-26 10:05:58 +02:00
Christoph J. Scherr ff9306047e Merge branch 'devel' of https://git.cscherr.de/PlexSheep/autocrate into devel
cargo devel CI / cargo CI (push) Successful in 2m38s Details
2024-04-26 10:04:33 +02:00
Christoph J. Scherr 181954cbce remodeling our structure with git a bit 2024-04-26 10:04:30 +02:00
Christoph J. Scherr 5bb4072e24 remodeling our structure with git a bit 2024-04-26 10:03:52 +02:00
PlexSheep 9aa9c8e07b automatic cargo CI changes 2024-04-26 06:58:04 +00:00
PlexSheep e767752338 automatic cargo CI changes 2024-04-26 06:57:17 +00:00
Christoph J. Scherr e80b68f1c4 Merge branch 'devel'
cargo devel CI / cargo CI (push) Successful in 3m4s Details
2024-04-26 08:55:01 +02:00
Christoph J. Scherr e66550f2ef github should maybe work
cargo devel CI / cargo CI (push) Successful in 3m4s Details
2024-04-26 08:54:23 +02:00
PlexSheep abe2e25071 automatic cargo CI changes 2024-04-26 06:36:24 +00:00
Christoph J. Scherr bf2b9c6d08 bump libpt
cargo devel CI / cargo CI (push) Successful in 2m15s Details
2024-04-26 08:34:13 +02:00
Christoph J. Scherr 378b269fa0 release to forgejo works in an early state
cargo devel CI / cargo CI (push) Failing after 1m41s Details
2024-04-25 15:46:37 +02:00
Christoph J. Scherr 1bc34011c1 release to forgejo works in an early state
cargo devel CI / cargo CI (push) Failing after 1m53s Details
2024-04-25 15:44:55 +02:00
Christoph J. Scherr 0e4d694b35 fix config parser
cargo devel CI / cargo CI (push) Failing after 1m41s Details
2024-04-25 14:33:48 +02:00
Christoph J. Scherr 926f72b87e
update cargo ci
cargo devel CI / cargo CI (push) Failing after 1m51s Details
2024-03-03 14:02:34 +01:00
Christoph J. Scherr 51a50cd13f
Merge branch 'master' into devel
cargo devel CI / cargo CI (push) Successful in 4m1s Details
2024-02-27 09:15:27 +01:00
PlexSheep 90312a457b automatic cargo CI changes 2024-02-25 00:18:12 +00:00
Christoph J. Scherr 2f8442bf74
we can tag and push, but the api does nothing?
cargo devel CI / cargo CI (push) Successful in 1m51s Details
2024-02-25 01:16:19 +01:00
Christoph J. Scherr 0c2f270336
Merge branch 'devel' of https://git.cscherr.de/PlexSheep/autocrate into devel
cargo devel CI / cargo CI (push) Successful in 1m54s Details
2024-02-25 00:15:13 +01:00
Christoph J. Scherr 9839b8b034
fix yaml parser 2024-02-25 00:15:11 +01:00
PlexSheep 23c51ac1da automatic cargo CI changes 2024-02-24 23:03:06 +00:00
Christoph J. Scherr 7d4a3986a9
in THEORY maybe we could release if we ask nicely
cargo devel CI / cargo CI (push) Successful in 2m3s Details
2024-02-25 00:01:03 +01:00
Christoph J. Scherr 2c26a65a65
security
cargo devel CI / cargo CI (push) Successful in 1m55s Details
2024-02-24 18:48:30 +01:00
Christoph J. Scherr 3e9b58da62
codeberg mirror note
cargo devel CI / cargo CI (push) Successful in 1m59s Details
2024-02-24 16:58:24 +01:00
Christoph J. Scherr 6e9183af20
update readme
cargo devel CI / cargo CI (push) Successful in 2m1s Details
2024-02-24 16:52:16 +01:00
Christoph J. Scherr 6e5582cbe6
update readme
cargo devel CI / cargo CI (push) Successful in 3m51s Details
2024-02-24 16:52:12 +01:00
Christoph J. Scherr ea50984719
typo
cargo devel CI / cargo CI (push) Successful in 3m2s Details
2024-02-24 14:53:05 +01:00
Christoph J. Scherr 6f70057aba
typo
cargo devel CI / cargo CI (push) Successful in 2m59s Details
2024-02-24 14:52:29 +01:00
PlexSheep a42393864e automatic cargo CI changes 2024-02-24 13:44:06 +00:00
Christoph J. Scherr 3e0d32721b
remove many deps
cargo devel CI / cargo CI (push) Successful in 1m55s Details
2024-02-24 14:41:56 +01:00
Christoph J. Scherr 45ba3f6b97
contexts for api actions
cargo devel CI / cargo CI (push) Failing after 4m0s Details
2024-02-24 14:33:58 +01:00
Christoph J. Scherr 39538b7b01
Merge branch 'devel' of https://git.cscherr.de/PlexSheep/autocrate into devel
cargo devel CI / cargo CI (push) Failing after 1m51s Details
2024-02-24 14:11:00 +01:00
Christoph J. Scherr d02830b656
a lot more to do 2024-02-24 14:10:44 +01:00
PlexSheep 19d5d11b05 automatic cargo CI changes 2024-02-24 12:28:29 +00:00
Christoph J. Scherr bd04eea9cc
pass enum
cargo devel CI / cargo CI (push) Successful in 4m4s Details
2024-02-24 13:24:24 +01:00
Christoph J. Scherr f12f5763d1
Merge branch 'devel' of https://git.cscherr.de/PlexSheep/autocrate into devel
cargo devel CI / cargo CI (push) Failing after 1m42s Details
2024-02-19 22:08:00 +01:00
Christoph J. Scherr 77f0aabdfc
important changes 2024-02-19 22:07:51 +01:00
PlexSheep 0abcdff5af automatic cargo CI changes 2024-02-16 19:08:27 +00:00
Christoph J. Scherr acc5381dd1
Merge branch 'devel' of https://git.cscherr.de/PlexSheep/autocrate into devel
cargo devel CI / cargo CI (push) Successful in 3m48s Details
2024-02-16 20:04:39 +01:00
Christoph J. Scherr 1c965da645
Merge branch 'devel' of https://git.cscherr.de/PlexSheep/autocrate into devel 2024-02-16 20:03:07 +01:00
Christoph J. Scherr b2bd3a9dde
Merge branch 'devel' of https://git.cscherr.de/PlexSheep/autocrate into devel
cargo devel CI / cargo CI (push) Failing after 1m38s Details
2024-02-16 20:01:17 +01:00
Christoph J. Scherr 0f1b82c4c9
release and server api skeletons 2024-02-16 20:00:55 +01:00
PlexSheep 48008c4d7a automatic cargo CI changes 2024-02-16 18:01:54 +00:00
Christoph J. Scherr 2568d96ac3
module structure
cargo devel CI / cargo CI (push) Successful in 3m34s Details
2024-02-16 18:58:20 +01:00
Christoph J. Scherr 660d4bd9da
Merge branch 'devel' of https://git.cscherr.de/PlexSheep/autocrate into devel
cargo devel CI / cargo CI (push) Successful in 3m43s Details
2024-02-16 18:40:23 +01:00
Christoph J. Scherr c5c79b784a
tokio 2024-02-16 18:37:19 +01:00
PlexSheep db14cb7a8f automatic cargo CI changes 2024-02-16 15:43:33 +00:00
Christoph J. Scherr 02c51d6e4a
ci add clippy rustup
cargo devel CI / cargo CI (push) Successful in 8m32s Details
2024-02-16 16:35:04 +01:00
Christoph J. Scherr 8c9e893d44
clippy ci
cargo devel CI / cargo CI (push) Failing after 23s Details
2024-02-16 16:12:00 +01:00
Christoph J. Scherr 0f6ca5d6f2
fix ci
cargo devel CI / cargo CI (push) Successful in 6m57s Details
2024-02-09 18:37:14 +01:00
Christoph J. Scherr 6267b0af45
fix release script
cargo devel CI / cargo CI (push) Successful in 4m32s Details
2024-02-02 00:06:32 +01:00
23 changed files with 812 additions and 118 deletions

View File

@ -1,4 +1,5 @@
---
version:
!cargo
changelog:
enable: true
git-log: true
@ -14,13 +15,16 @@ uses:
api:
github:
type: github
endpoint: https://github.com
repository: autocrate
auth:
user: myUserName
pass: token_superimportantsecret
myserv:
type: gitea
user: PlexSheep
pass:
!env TOKEN_GH
cscherr:
type: forgejo
endpoint: https://git.cscherr.de
repository: autocrate
auth:
user: myUserName
pass: importantsecrettoken
user: PlexSheep
pass:
!env TOKEN_CSCHERR

View File

@ -6,7 +6,7 @@ on:
# - '!master'
jobs:
CI:
format:
name: cargo CI
permissions:
# Give the default GITHUB_TOKEN write permission to commit and push the
@ -16,9 +16,11 @@ jobs:
- name: get repo
uses: actions/checkout@v4
- name: install rust
uses: dtolnay/rust-toolchain@stable
uses: https://github.com/dtolnay/rust-toolchain@stable
- name: install additional rust things
run: rustup component add rustfmt
run: |
rustup component add rustfmt
rustup component add clippy
- name: config custom registry
run: |
mkdir -p ~/.cargo/
@ -28,16 +30,16 @@ jobs:
echo '[registries.cscherr]' >> ~/.cargo/config.toml
echo 'index = "https://git.cscherr.de/PlexSheep/_cargo-index.git"' >> ~/.cargo/config.toml
cat ~/.cargo/config.toml
- name: cargo check
run: cargo check --all-features --all-targets
- name: cargo fix
run: cargo fix --all-features --all-targets
- name: cargo clippy check
run: cargo clippy --all-features --all-targets --workspace
- name: cargo clippy fix
run: cargo clippy --fix --all-features --all-targets --workspace
- name: cargo fmt
run: cargo fmt --all
- name: cargo test
run: cargo test --all-features --all-targets
run: cargo test --all-features --all-targets --workspace
- name: commit back to repository
uses: stefanzweifel/git-auto-commit-action@v5
uses: https://github.com/stefanzweifel/git-auto-commit-action@v5
with:
# Optional. Commit message for the created commit.
# Defaults to "Apply automatic changes"

View File

@ -19,7 +19,9 @@ jobs:
- name: install rust
uses: dtolnay/rust-toolchain@stable
- name: install additional rust things
run: rustup component add rustfmt
run: |
rustup component add rustfmt
rustup component add clippy
- name: config custom registry
run: |
mkdir -p ~/.cargo/
@ -29,14 +31,14 @@ jobs:
echo '[registries.cscherr]' >> ~/.cargo/config.toml
echo 'index = "https://git.cscherr.de/PlexSheep/_cargo-index.git"' >> ~/.cargo/config.toml
cat ~/.cargo/config.toml
- name: cargo check
run: cargo check --all-features --all-targets
- name: cargo fix
run: cargo fix --all-features --all-targets
- name: cargo clippy check
run: cargo clippy --all-features --all-targets --workspace
- name: cargo clippy fix
run: cargo clippy --fix --all-features --all-targets --workspace
- name: cargo fmt
run: cargo fmt --all
- name: cargo test
run: cargo test --all-features --all-targets
run: cargo test --all-features --all-targets --workspace
- name: commit back to repository
uses: stefanzweifel/git-auto-commit-action@v5
with:

1
.gitignore vendored
View File

@ -19,3 +19,4 @@ Cargo.lock
# Added by cargo
/target
.env

View File

@ -1,6 +1,6 @@
[package]
name = "autocrate"
version = "0.1.0-prealpha.1"
version = "0.1.0-prealpha.5"
edition = "2021"
publish = true
authors = ["Christoph J. Scherr <software@cscherr.de>"]
@ -21,15 +21,22 @@ keywords = [
[dependencies]
anyhow = "1.0.79"
cargo = "0.76.0"
async-trait = "0.1.77"
# cargo = "0.76.0"
clap = { version = "4.4.18", features = ["derive", "help"] }
clap-verbosity-flag = "2.1.2"
forgejo-api = "0.1.0"
futures = "0.3.30"
git2 = "0.18.1"
libpt = { version = "0.3.11", features = ["log"] }
libpt = { version = "0.4.2", features = ["log"] }
octocrab = "0.38.0"
reqwest = "0.11.24"
serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.116"
serde_yaml = "0.9.30"
tempfile = "3.9.0"
thiserror = "1.0.56"
tokio = { version = "1.36.0", features = ["tokio-macros", "rt-multi-thread", "macros"] }
url = { version = "2.5.0", features = ["serde"] }
[[bin]]

123
README.md
View File

@ -8,47 +8,65 @@
![logo](data/media/autocrate.jpeg)
**Disclaimer**: I've generated the Readme and logo with the help of so called AI
tools and modified them afterwards.
Autocrate simplifies the creation and maintenance of releases for your Rust
projects hosted on Gitea servers. By providing essential functionalities
like uploading artifacts, publishing crates, and managing changelogs,
Autocrate streamlines the release process, allowing developers to focus on
their work. Although initially built for Gitea, we plan to extend support
for additional platforms such as GitHub and GitLab.
projects hosted on fancy git servers. By providing functionalities
like creating releases uploading artifacts, publishing crates, and managing changelogs,
Autocrate tries to streamline the release process. Although initially built for Forgejo,
I plan to extend support to other platforms such as GitHub and GitLab.
Autocrate can then be used in CI/CD, or in projects without
continuous integration to release software.
The software is built in Rust, and offers integration for Rust Projects with Cargo.
In the future, using other tools and specifying custom scripts will become possible.
* [Original Repository](https://git.cscherr.de/PlexSheep/Autocrate)
* [Codeberg Mirror](https://codeberg.org/PlexSheep/autocrate)
* [GitHub Mirror](https://github.com/PlexSheep/Autocrate)
* [crates.io](https://crates.io/crates/autocrate)
* [docs.rs](https://docs.rs/crate/autocrate/)
Take a look at the [scripts](./scripts) directory! [publish.sh](scripts/publish.sh)
and [release.sh](scripts/release.sh) are exactly what I'm trying to get rid of.
and [release.sh](scripts/release.sh) are what I'm trying to get rid of.
## Features
* Create and update releases on your Gitea server
* Publish crates to Cargo.rs
or other repositories directly from your Rust projects
* Upload artifacts, including documentation and binaries, alongside your releases
* Generate and maintain changelogs and release notes.
* Create and update releases on your Git platform
* Publish crates to crates.io or other repositories
* Upload artifacts, including binaries and signatures alongside your releases
* Generate changelogs and release notes
* Configure with a simple yaml file
### Upcoming Features
My goal is to continuously enhance Autocrate to better serve the developer
community. Some planned improvements include supporting other popular hosting
platforms, enabling even greater flexibility and convenience.
Autocrate is still in pre-alpha, so the features listed above are still being
worked on. For the future, the following Features are planned:
* Support for platforms other than Forgejo
* Custom artifact build scripts
* Version bumping
* Interactive and scriptable CLI interface
* Publish a cargo workspace (that depends on it's own crates)
## Getting Started
Before getting started with Autocrate, make sure you have the necessary
prerequisites covered:
* **A Rust Environment**: Install the latest stable Rust compiler and
associated tools via the official website: <https://www.rust-lang.org/>
* **Access to a Gitea Server** (such as [git.cscherr.de](https://git.cscherr.de)
and [codeberg.org](https://codeberg.org) (strictly speaking, uses a Gitea fork)
You can use `autocrate init` to set your workspace up with a basic
`.autocrate.yaml`.
* **Access to a supported Git Server** (such as [git.cscherr.de](https://git.cscherr.de)
and [codeberg.org](https://codeberg.org))
### Pre-requisites
* Git
#### If you want to compile it yourself
Install Rust, the officially recommended way is through [rustup.rs](https://rustup.rs/).
Your distribution may offer a Rust distribution in your package manager as an alternative
### Installing
@ -65,21 +83,24 @@ locally, making it readily accessible through your command line interfaces.
Create a YAML file named `.autocrate.yml` (or `.yaml`) in the root of your Git
repository. It should contain the following parameters (replace the placeholders):
| Parent | Key | Value | Explanation |
|-----------------|--------------|----------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| (root) | `changelog` | list of keys with this as parent (`git-log` etc) | information on how a changelog is generated |
| `changelog` | `enable` | `true`/`false` | If false, no changelog will be generated |
| `changelog` | `git-log` | `true`/`false` | should a changelog be generated with `git log`? |
| (root) | `uses` | list of keys with this as parent (`cargo` etc) | Marks features to be used by Autocrate |
| `uses` | `cargo` | list of keys with this as parent (`publish` etc) | tells us that your project uses cargo |
| `cargo` | `publish` | `true`/`false` | should we publish crates? |
| `cargo` | `registries` | registries see [this](https://doc.rust-lang.org/cargo/reference/registries.html) | A list of registries we should publish to. If empty defaults to `crates.io`. |
| (root) | `api` | list of names, which each have the same keys | defines the api we talk to |
| `api.NAME` | `type` | one of `gitea`,`github`,`gitlab` (currently only support for `gitea` | Let's us know which api type we are talking to |
| `api.NAME` | `endpoint` | Base URL of the target server | Let's us know which api type we are talking to |
| `api.NAME` | `auth` | list of keys with this as parent (`user` and `pass`) | We probably need authentication on the target server |
| `api.NAME.auth` | `user` | a string | Which user should we try to authenticate as |
| `api.NAME.auth` | `pass` | a string | A secret for authentication o the server, probably a token |
| Parent | Key | Value | Explanation |
|----------------------|--------------|----------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| (root) | `changelog` | list of keys with this as parent (`git-log` etc) | information on how a changelog is generated |
| `changelog` | `enable` | `true`/`false` | If false, no changelog will be generated |
| `changelog` | `git-log` | `true`/`false` | should a changelog be generated with `git log`? |
| (root) | `uses` | list of keys with this as parent (`cargo` etc) | Marks features to be used by Autocrate |
| `uses` | `cargo` | list of keys with this as parent (`publish` etc) | tells us that your project uses cargo |
| `cargo` | `publish` | `true`/`false` | should we publish crates? |
| `cargo` | `registries` | registries see [this](https://doc.rust-lang.org/cargo/reference/registries.html) | A list of registries we should publish to. If empty defaults to `crates.io`. |
| (root) | `api` | list of names, which each have the same keys | defines the api we talk to |
| `api.NAME` | `type` | one of `gitea`,`github`,`gitlab` (currently only support for `gitea` | Let's us know which api type we are talking to |
| `api.NAME` | `endpoint` | Base URL of the target server | Let's us know which api type we are talking to |
| `api.NAME` | `auth` | list of keys with this as parent (`user` and `pass`) | We probably need authentication on the target server |
| `api.NAME.auth` | `user` | a string | Which user should we try to authenticate as |
| `api.NAME.auth` | `pass` | contains either of `text`, `env` or `file` | sets the secret for authentication with this server |
| `api.NAME.auth.pass` | `text` | a authentication pass as clear text | A secret for authentication of the server, probably a token |
| `api.NAME.auth.pass` | `env` | env var which contains the token | A secret for authentication of the server, probably a token |
| `api.NAME.auth.pass` | `file` | file var which contains the token | A secret for authentication of the server, probably a token |
An example `.autocrate.yaml` could look like this:
@ -102,22 +123,28 @@ api:
endpoint: https://github.com
auth:
user: PlexSheep
pass: token_superimportantsecret
pass:
text: token_superimportantsecret
cscherr:
type: gitea
endpoint: https://git.cscherr.de
auth:
user: PlexSheep
pass: Bearer importantsecrettoken
pass:
file: secrettoken.txt
```
After Autocrate has been bootstrapped, you it will be released and published
After Autocrate has been bootstrapped, it will be released and published
with itself, so you can take a look at this repositories
[`.autocrate.yaml`](./.autocrate.yaml).
## Using Autocrate
TBD
After you have your workspace with a `.autocrate.yaml` file, you can:
* `autocrapte release` to create a release on your git server(s), optionally publishing too
* `autocrate publish` to publish your crate to the specified registries(s) (default is crates.io)
* `autocrate changelog` to generate a changelog since the last tag
## Licensing
@ -128,13 +155,23 @@ License. Please refer to [`LICENSE`](./LICENSE) for complete licensing details.
## Project status
The project has started recently and is currently in pre-alpha.
The project has started recently and is currently in pre-alpha. Many features
are still missing or experimental
## Contributing
I'd be very happy to get contributions! Although the master repository is on
my self hosted git server, you're free to create issues, PRs and so on on
GitHub. If enough activity comes around, moving to GitHub might be a good idea.
my self hosted git server, you're free to create issues, PRs and so on
GitHub. If enough activity comes around, moving to GitHub Codeberg might be a
good idea.
If you have any questions, use issues and discussions tabs or write me an email
to [software@cscherr.de](mailto:software@cscherr.de)
## Security
If you find a security issue with this repository, it would be best if you sent
me a mail to [software@cscherr.de](mailto:software@cscherr.de) or reported it on
GitHub.
See [`SECURITY`](SECURITY.md).

14
SECURITY.md Normal file
View File

@ -0,0 +1,14 @@
# Security Policy
## Supported Versions
Only the latest release is currently supported.
## Reporting a Vulnerability
It would be best if you reported any found security vulnerabilities on the GitHub mirror.
If you want to send something encrypted, use [this](https://static.cscherr.de/keys/software@cscherr.de.asc) key.
You can always reach me on [software@cscherr.de](mailto:software@cscherr.de), but sadly, I'm struggeling with PGP
encryption in my mail client. I use protonmail to host my EMail services, so if you send me something from protonmail,
it will be end to end encrypted.

View File

@ -7,7 +7,7 @@ BODY="
$(git log $(git describe --tags --abbrev=0)..HEAD --pretty="- %s" --oneline --decorate)
"
USER=PlexSheep
git tag "v$NEW_VERSION-test" || echo "could not tag"
git tag "v$NEW_VERSION" || echo "could not tag"
curl -X 'POST' \
'https://git.cscherr.de/api/v1/repos/PlexSheep/'$REPO'/releases' \
-H 'accept: application/json' \

View File

@ -17,6 +17,7 @@ impl Changelog {
Ok(Changelog { git_log })
}
// TODO: use libgit2 instead of the cli interface
fn make_git_log(cfg: &Config) -> Result<Option<String>> {
if !cfg.yaml.changelog.enable {
return Ok(None);
@ -28,14 +29,13 @@ impl Changelog {
let out = cmd.output()?;
// FIXME: this does not catch fancy colors, those are from the shell as it seems? I don't
// get it.
let buf = String::from_utf8(out.stdout).map_err(|err| ChangelogError::GitUTF8Error(err))?;
let buf = String::from_utf8(out.stdout).map_err(ChangelogError::GitUTF8Error)?;
if !out.status.success() {
// TODO: get the stderr for error reporting
// TODO: Make the error more understandable for the user
return Err(ChangelogError::GitBadStatus(out.status, buf).into());
}
dbg!(&buf);
Ok(Some(buf))
}
@ -43,14 +43,14 @@ impl Changelog {
let mut cmd = Command::new("git");
cmd.arg("describe").arg("--tags").arg("--abbrev=0");
let out = cmd.output()?;
let buf = String::from_utf8(out.stdout).map_err(|err| ChangelogError::GitUTF8Error(err))?;
let buf = String::from_utf8(out.stdout).map_err(ChangelogError::GitUTF8Error)?;
if !out.status.success() {
// TODO: get the stderr for error reporting
// TODO: Make the error more understandable for the user
return Err(ChangelogError::GitBadStatus(out.status, buf).into());
}
let buf = buf.replace("\n", "");
return Ok(buf);
let buf = buf.replace('\n', "");
Ok(buf)
}
}

View File

@ -38,6 +38,7 @@ pub struct Cli {
#[derive(Debug, Clone, Subcommand)]
pub enum Commands {
Changelog {},
/// Create a new release on the server
Release {
// FIXME: allow taking a message like this:
// `autocrate changelog -m arg1 arg2 arg3`
@ -52,14 +53,31 @@ pub enum Commands {
//
// TODO:
// find a way to make this a global option but only usable with specific subcommands
//
// TODO:
// integrate a CHANGELOG.md file
//
/// Message body of the release
#[arg(short, long)]
message: Option<Vec<String>>,
/// generate and add a changelog
#[arg(short, long)]
changelog: bool,
/// publish after releasing
#[arg(short, long)]
publish: bool,
},
/// Publish to a package registry
Publish {
// see Commands::Release { message }
#[arg(short, long)]
message: Option<Vec<String>>,
},
///
Version {},
Init {},
}
impl Display for Commands {
@ -71,6 +89,8 @@ impl Display for Commands {
Self::Changelog { .. } => "Changelog",
Self::Release { .. } => "Release",
Self::Publish { .. } => "Publish",
Self::Version { .. } => "Version",
Self::Init { .. } => "Init",
}
)
}
@ -90,11 +110,11 @@ impl Cli {
}
};
if cli.meta {
Logger::init(None, Some(ll), true).expect("could not initialize Logger");
Logger::build(None, Some(ll), true).expect("could not initialize Logger");
} else {
// less verbose version
Logger::init_mini(Some(ll)).expect("could not initialize Logger");
Logger::build_mini(Some(ll)).expect("could not initialize Logger");
}
return cli;
cli
}
}

View File

@ -1,20 +1,24 @@
use std::{collections::HashMap, fmt::Debug, fs::File, io::BufReader, path::PathBuf};
use std::str::FromStr;
use std::{
collections::HashMap, fmt::Debug, fs::File, io::BufReader, path::PathBuf, process::Command,
};
use git2;
use libpt::log::{debug, error, trace};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::error::*;
pub mod cli;
pub mod packages;
use cli::Cli;
pub trait YamlConfigSection: Debug + Clone + for<'a> Deserialize<'a> {
fn check(&self) -> Result<()>;
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Changelog {
pub enable: bool,
#[serde(alias = "git-log")]
@ -26,7 +30,7 @@ impl YamlConfigSection for Changelog {
}
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UseCargo {
pub publish: bool,
pub registries: Vec<String>,
@ -37,7 +41,7 @@ impl YamlConfigSection for UseCargo {
}
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Uses {
cargo: UseCargo,
}
@ -48,43 +52,100 @@ impl YamlConfigSection for Uses {
}
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Pass {
/// pass specified as plainext
#[serde(alias = "pass_text")]
Text(String),
/// pass to be loaded from an env var
#[serde(alias = "pass_env")]
Env(String),
/// pass to be loaded from a file
#[serde(alias = "pass_file")]
File(PathBuf),
}
impl Pass {
/// Get the pass, extracting from the underlying source
pub fn get_pass(&self) -> Result<String> {
self.check()?;
Ok(match self {
Self::Text(pass) => pass.clone(),
Self::Env(key) => std::env::var(key).map_err(ConfigError::from)?,
Self::File(file) => std::fs::read_to_string(file)?,
})
}
}
impl YamlConfigSection for Pass {
fn check(&self) -> Result<()> {
match self {
Self::Text(_) => (),
Self::Env(envvar) => {
if !std::env::var(envvar).map_err(ConfigError::from)?.is_empty() {
} else {
return Err(ConfigError::EnvNotSet(envvar.clone()).into());
}
}
Self::File(file) => {
if !file.exists() {
return Err(ConfigError::PassFileDoesNotExist(file.clone()).into());
}
}
};
Ok(())
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ApiAuth {
pub user: String,
pub pass: Option<String>,
pub pass_file: Option<PathBuf>,
pub pass: Pass,
}
impl YamlConfigSection for ApiAuth {
fn check(&self) -> Result<()> {
if self.pass.is_some() && self.pass_file.is_some() {
let err = ConfigError::YamlApiAuthBothPass(self.clone()).into();
error!("{err}");
return Err(err);
}
if self.pass_file.is_some() {
let file = self.pass_file.clone().unwrap();
if !file.exists() {
return Err(ConfigError::PassFileDoesNotExist(file).into());
}
}
self.pass.check()?;
Ok(())
}
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Api {
pub r#type: ApiType,
pub endpoint: Url,
#[serde(alias = "type")]
pub server_type: ApiType,
/// May be left empty if the [ApiType] is [Github](ApiType::Github).
pub endpoint: Option<Url>,
/// May be left empty if the Api does not need auth or the auth is part of the
/// [endpoint](Api::endpoint) [Url].
pub auth: Option<ApiAuth>,
/// Name of the repository on the Git server, as git itself has no concept of repository name
pub repository: String,
}
impl std::fmt::Display for Api {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.server_type {
ApiType::Github => write!(
f,
"{}",
self.server_type
.default_endpoint()
.expect("no default endpoint set for github")
),
_ => write!(f, "{}", self.endpoint.clone().expect("no endpoint set")),
}
}
}
impl YamlConfigSection for Api {
fn check(&self) -> Result<()> {
self.r#type.check()?;
match self.endpoint.socket_addrs(|| None) {
Ok(_) => (),
Err(err) => return Err(err.into()),
self.server_type.check()?;
if self.server_type != ApiType::Github {
if self.auth.is_none() {
return Err(ConfigError::NoEndpointSet.into());
}
match self.endpoint.clone().unwrap().socket_addrs(|| None) {
Ok(_) => (),
Err(err) => return Err(err.into()),
}
} else if let Some(_url) = &self.endpoint {
return Err(ConfigError::EndpointSetButNotNeeded.into());
}
if self.auth.is_some() {
self.auth.clone().unwrap().check()?;
@ -93,7 +154,7 @@ impl YamlConfigSection for Api {
}
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub enum ApiType {
#[serde(alias = "gitea")]
Gitea,
@ -101,23 +162,100 @@ pub enum ApiType {
Gitlab,
#[serde(alias = "github", alias = "GitHub")]
Github,
#[serde(alias = "forgejo")]
Forgejo,
}
impl ApiType {
pub fn default_endpoint(&self) -> Option<Url> {
match self {
Self::Github => Some(Url::from_str("https://github.com").unwrap()),
_ => None,
}
}
}
impl YamlConfigSection for ApiType {
fn check(&self) -> Result<()> {
Ok(())
}
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Version {
Text(String),
Cmd(String),
Cargo,
}
impl Version {
pub fn get_version(&self) -> String {
// TODO: Error handling
match self {
Self::Text(ver) => ver.clone(),
Self::Cmd(shell_command) => {
match Command::new("/bin/bash")
.arg("-c")
.arg(shell_command)
.output()
{
Ok(output) => {
// TODO: check status
String::from_utf8(output.stdout).unwrap()
}
Err(err) => {
panic!("{err:?}");
}
}
}
Self::Cargo => {
match Command::new("/bin/bash")
.arg("-c")
.arg(r#"cat Cargo.toml | rg '^\s*version\s*=\s*"([^"]*)"\s*$' -or '$1'"#)
.output()
{
Ok(output) => {
// TODO: check status
String::from_utf8(output.stdout).unwrap().trim().to_owned()
}
Err(err) => {
panic!("{err:?}");
}
}
}
}
}
}
impl YamlConfigSection for Version {
fn check(&self) -> Result<()> {
match self {
Self::Text(_) => (),
Self::Cmd(_cmd) => {
// TODO: get the version with a command
todo!("verion from cmd not implemented")
}
Self::Cargo => {
// TODO: get the version as specified in a Cargo.toml
todo!("verion from cargo not implemented")
}
}
Ok(())
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct YamlConfig {
pub changelog: Changelog,
pub uses: Uses,
pub api: HashMap<String, Api>,
pub version: Version,
}
impl YamlConfigSection for YamlConfig {
fn check(&self) -> Result<()> {
self.changelog.check()?;
self.uses.check()?;
self.version.check()?;
for api in self.api.values() {
api.check()?;
}
@ -135,10 +273,10 @@ impl YamlConfig {
}
}
#[derive(Clone)]
pub struct Config {
pub yaml: YamlConfig,
pub cli: Cli,
pub repo: git2::Repository,
pub path: PathBuf,
}
@ -147,7 +285,7 @@ impl Debug for Config {
write!(
f,
"{}",
format!(
format_args!(
"Config {{yaml: {:?}, repo_path: {:?}}}",
self.yaml, self.path
)
@ -156,7 +294,7 @@ impl Debug for Config {
}
impl Config {
pub fn load(cli: Cli) -> Result<Self> {
pub fn load(cli: &Cli) -> Result<Self> {
let repo = match git2::Repository::open_from_env() {
Ok(repo) => repo,
Err(_err) => {
@ -195,9 +333,8 @@ impl Config {
Ok(Config {
yaml,
repo,
path,
cli,
cli: cli.clone(),
})
}
}

3
src/config/packages.rs Normal file
View File

@ -0,0 +1,3 @@
pub enum PackageType {
Cargo,
}

View File

@ -1,4 +1,4 @@
use std::{path::PathBuf, process::ExitStatus, string::FromUtf8Error};
use std::{env::VarError, path::PathBuf, process::ExitStatus, string::FromUtf8Error};
use anyhow;
use thiserror::Error;
@ -20,6 +20,22 @@ pub enum Error {
SerdeYaml(#[from] serde_yaml::Error),
#[error("Could not generate the changelog")]
ChangelogError(#[from] ChangelogError),
#[error("Server Api error")]
ServerApiError(#[from] ServerApiError),
}
#[derive(Error, Debug)]
pub enum ServerApiError {
#[error(transparent)]
ParseUrl(#[from] url::ParseError),
#[error(transparent)]
InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue),
#[error(transparent)]
ReqwestErr(#[from] reqwest::Error),
#[error(transparent)]
ForgejoApiError(#[from] forgejo_api::ForgejoError),
#[error(transparent)]
GithubApiError(#[from] octocrab::Error),
}
#[derive(Error, Debug)]
@ -45,4 +61,12 @@ pub enum ConfigError {
YamlApiAuthBothPass(ApiAuth),
#[error("password provided as file, but does not exist: {0}")]
PassFileDoesNotExist(PathBuf),
#[error("config requires environment variable {0}, but {0} is not set")]
EnvNotSet(String),
#[error("Bad value for environment variable: {0}")]
BadEnv(#[from] VarError),
#[error("An endpoint was set for an ApiType that does not require one")]
EndpointSetButNotNeeded,
#[error("No endpoint was set for an ApiType that requires one")]
NoEndpointSet,
}

66
src/git/mod.rs Normal file
View File

@ -0,0 +1,66 @@
use std::process::Command;
use git2;
use libpt::log::error;
use crate::error::ConfigError;
use crate::{config::Config, error::Result};
pub(crate) fn get_repo() -> Result<git2::Repository> {
let repo = match git2::Repository::open_from_env() {
Ok(repo) => repo,
Err(_err) => {
let err = ConfigError::GitRepoNotFound.into();
error!("{err}");
return Err(err);
}
};
Ok(repo)
}
pub async fn tag<'repo>(
repo: &'repo mut git2::Repository,
cfg: &Config,
) -> Result<git2::Tag<'repo>> {
// TODO: error handling
// TODO: allow force
// TODO: allow setting a message
// TODO: maybe using git as cmd is fancier?
let target = repo
.find_object(
repo.head().unwrap().target().unwrap(),
Some(git2::ObjectType::Commit),
)
.unwrap();
let tagger = repo.signature().expect("could not get signature");
let message = String::new();
let force = true;
let tag = repo
.tag(
&cfg.yaml.version.get_version(),
// "importantversion",
&target,
&tagger,
&message,
force,
)
.unwrap();
let tag: git2::Tag = repo.find_tag(tag).unwrap();
Ok(tag)
}
pub async fn push(_cfg: &Config) -> Result<()> {
// TODO: error handling
// TODO: maybe using git as lib is fancier?
Command::new("git").arg("push").status().unwrap();
Ok(())
}
pub async fn get_commit_sig<'repo>(repo: &'repo git2::Repository) -> Result<String> {
// TODO: error handling
// TODO: maybe using git as cmd is fancier?
let target = repo
.find_commit(repo.head().unwrap().target().unwrap())
.unwrap();
Ok(target.id().to_string())
}

View File

@ -1,3 +1,7 @@
pub mod changelog;
pub mod config;
pub mod error;
pub mod git;
pub mod publish;
pub mod release;
pub mod serverapi;

View File

@ -1,3 +1,5 @@
use std::error::Error as _;
use autocrate::{
changelog::*,
config::{
@ -5,22 +7,56 @@ use autocrate::{
Config,
},
error::*,
publish::publish,
release::release,
serverapi::ApiCollection,
};
use libpt::log::{debug, error};
fn main() -> Result<()> {
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::cli_parse();
let cfg = Config::load(cli.clone())?;
let cfg = Config::load(&cli)?;
match cli.command {
let status: Option<Error> = match cli.command {
Commands::Changelog { .. } => {
println!("{}", Changelog::build(&cfg)?.to_string());
Ok(())
let chlog = Changelog::build(&cfg);
if chlog.is_ok() {
println!("{}", chlog.unwrap());
None
} else {
Some(chlog.unwrap_err())
}
}
Commands::Release { .. } => {
todo!()
// TODO: check if repo is dirty and create a commit with a given option
let mut apis = ApiCollection::build(&cfg).await?;
match release(&cfg, &mut apis).await {
Ok(_) => None,
Err(err) => Some(err),
}
}
Commands::Publish { .. } => {
// TODO: check if repo is dirty and create a commit with a given option
match publish(&cfg).await {
Ok(_) => None,
Err(err) => Some(err),
}
}
Commands::Version {} => {
// TODO: version bump
// TODO: version select interactive
// TODO: version select automated
todo!()
}
Commands::Init { .. } => {
// TODO: create a basic autocrate yaml
todo!()
}
};
if let Some(err) = status {
error!("{err}");
debug!("{:#?}", err.source());
}
Ok(())
}

7
src/publish/mod.rs Normal file
View File

@ -0,0 +1,7 @@
use crate::{config::Config, error::*};
pub struct PublishContext;
pub async fn publish(_cfg: &Config) -> Result<()> {
todo!()
}

66
src/release/mod.rs Normal file
View File

@ -0,0 +1,66 @@
use crate::{
config::Config,
error::*,
git::{self, get_commit_sig, push, tag},
serverapi::ApiCollection,
};
use futures::{self, stream::FuturesUnordered, StreamExt};
use libpt::log::info;
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct ReleaseContext {
pub draft: bool,
pub prerelease: bool,
pub username: String,
pub repository: String,
pub text: String,
pub tag: String,
pub commit_sig: String,
}
pub async fn release(cfg: &Config, apis: &mut ApiCollection) -> Result<()> {
// TODO: Error handling
let _changelog = crate::changelog::Changelog::build(cfg)?.to_string();
let mut repo = git::get_repo()?;
let tag = tag(&mut repo, cfg).await?.name().unwrap().to_string();
let commit_sig = get_commit_sig(&repo).await?;
push(cfg).await?; // we assume that we only need to push the current branch to the singular
// remote, expecting that the repositories are somehow mirrored
// TODO: push to multiple remotes?
let mut results = FuturesUnordered::new();
for api in apis.iter_mut() {
// TODO: check that auth exists
let specific_rc = ReleaseContext {
draft: true,
prerelease: true,
username: api
.get_inner()
.clone()
.auth
.expect("no auth but trying to publish")
.user,
repository: api.get_inner().repository.clone(),
text: crate::changelog::Changelog::build(cfg)?.to_string(),
tag: tag.clone(),
commit_sig: commit_sig.clone(),
};
info!("pushing release for {}", api.get_inner());
results.push(api.push_release(specific_rc));
}
// wait for the release requests to finish
while let Some(result) = results.next().await {
if result.is_err() {
return Err(result.unwrap_err());
}
}
// TODO: check that the release is made
// TODO: generate artifacts
// TODO: upload artifacts
// TODO: upload artifact signatures
Ok(())
}

57
src/serverapi/forgejo.rs Normal file
View File

@ -0,0 +1,57 @@
use crate::{
config::{Api, Config},
error::*,
serverapi::{PublishContext, ReleaseContext, ServerApi},
};
use async_trait::async_trait;
use forgejo_api;
pub struct Forgejo {
api: Api,
cfg: Config,
api_wrapper: forgejo_api::Forgejo,
}
impl Forgejo {
pub async fn build(api: &Api, cfg: &Config) -> Result<Self> {
let api_wrapper: forgejo_api::Forgejo = forgejo_api::Forgejo::new(
forgejo_api::Auth::Token(&api.auth.clone().unwrap().pass.get_pass()?),
api.endpoint.clone().unwrap(),
)
.map_err(ServerApiError::from)?;
Ok(Self {
api: api.clone(),
cfg: cfg.clone(),
api_wrapper,
})
}
}
#[async_trait]
impl ServerApi for Forgejo {
async fn push_release(&mut self, rc: ReleaseContext) -> Result<()> {
let body: forgejo_api::structs::CreateReleaseOption =
forgejo_api::structs::CreateReleaseOption {
body: Some(rc.text),
draft: Some(rc.draft),
name: Some(rc.tag.clone()),
prerelease: Some(rc.prerelease),
tag_name: rc.tag,
target_commitish: Some(rc.commit_sig),
};
self.api_wrapper
.repo_create_release(&rc.username, &rc.repository, body)
.await
.map_err(ServerApiError::from)?;
Ok(())
}
async fn push_release_artifact(&mut self, _rc: ReleaseContext) -> Result<()> {
todo!()
}
async fn push_pkg(&mut self, _pc: PublishContext) -> Result<()> {
todo!()
}
fn get_inner(&self) -> &Api {
&self.api
}
}

32
src/serverapi/gitea.rs Normal file
View File

@ -0,0 +1,32 @@
use async_trait::async_trait;
use super::{PublishContext, ReleaseContext, ServerApi};
use crate::{
config::{Api, Config},
error::*,
};
pub struct Gitea {
api: Api,
}
#[async_trait]
impl ServerApi for Gitea {
async fn push_release(&mut self, _rc: ReleaseContext) -> Result<()> {
todo!()
}
async fn push_release_artifact(&mut self, _rc: ReleaseContext) -> Result<()> {
todo!()
}
async fn push_pkg(&mut self, _pc: PublishContext) -> Result<()> {
todo!()
}
fn get_inner(&self) -> &Api {
&self.api
}
}
impl Gitea {
pub async fn build(api: &Api, _cfg: &Config) -> Result<Self> {
Ok(Self { api: api.clone() })
}
}

47
src/serverapi/github.rs Normal file
View File

@ -0,0 +1,47 @@
use async_trait::async_trait;
use octocrab;
use super::{PublishContext, ReleaseContext, ServerApi};
use crate::{
config::{Api, Config},
error::*,
};
pub struct Github {
api: Api,
}
impl Github {
pub async fn build(api: &Api, _cfg: &Config) -> Result<Self> {
Ok(Self {
api: api.to_owned(),
})
}
}
#[async_trait]
impl ServerApi for Github {
async fn push_release(&mut self, rc: ReleaseContext) -> Result<()> {
let _response = octocrab::instance()
.repos(rc.username, rc.repository)
.releases()
.create(&rc.tag)
.target_commitish(&rc.commit_sig)
.name(&rc.tag)
.body(&rc.text)
.draft(rc.draft)
.prerelease(rc.prerelease)
.send()
.await
.map_err(ServerApiError::from)?;
Ok(())
}
async fn push_release_artifact(&mut self, _rc: ReleaseContext) -> Result<()> {
todo!()
}
async fn push_pkg(&mut self, _pc: PublishContext) -> Result<()> {
todo!()
}
fn get_inner(&self) -> &Api {
&self.api
}
}

32
src/serverapi/gitlab.rs Normal file
View File

@ -0,0 +1,32 @@
use async_trait::async_trait;
use super::{PublishContext, ReleaseContext, ServerApi};
use crate::{
config::{Api, Config},
error::*,
};
pub struct Gitlab {
api: Api,
}
#[async_trait]
impl ServerApi for Gitlab {
async fn push_release(&mut self, _rc: ReleaseContext) -> Result<()> {
todo!()
}
async fn push_release_artifact(&mut self, _rc: ReleaseContext) -> Result<()> {
todo!()
}
async fn push_pkg(&mut self, _pc: PublishContext) -> Result<()> {
todo!()
}
fn get_inner(&self) -> &Api {
&self.api
}
}
impl Gitlab {
pub async fn build(api: &Api, _cfg: &Config) -> Result<Self> {
Ok(Self { api: api.clone() })
}
}

96
src/serverapi/mod.rs Normal file
View File

@ -0,0 +1,96 @@
use std::ops::{Deref, DerefMut};
use async_trait::async_trait;
use reqwest::ClientBuilder;
use crate::{
config::{self, ApiType, Config},
error::*,
publish::PublishContext,
release::ReleaseContext,
};
pub mod forgejo;
pub mod gitea;
pub mod github;
pub mod gitlab;
use forgejo::*;
use gitea::*;
use github::*;
use gitlab::*;
pub static USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
// NOTE: in stable rust, traits can normally not contain async methods,
// see [here](https://stackoverflow.com/questions/65921581/how-can-i-define-an-async-method-in-a-trait).
// The `async_trait` crate can be used to work around this limitation.
#[async_trait]
pub trait ServerApi {
async fn push_release(&mut self, rc: ReleaseContext) -> Result<()>;
async fn push_release_artifact(&mut self, rc: ReleaseContext) -> Result<()>;
async fn push_pkg(&mut self, pc: PublishContext) -> Result<()>;
fn get_inner(&self) -> &config::Api;
}
pub(crate) type ApiCollectionInner = Vec<Box<dyn ServerApi>>;
pub struct ApiCollection {
collection: ApiCollectionInner,
}
impl ApiCollection {
pub async fn build(cfg: &Config) -> Result<Self> {
let mut collection: ApiCollectionInner = ApiCollectionInner::new();
for api in &cfg.yaml.api {
match api.1.server_type {
ApiType::Gitea => {
collection.push(Box::new(Gitea::build(api.1, cfg).await?));
}
ApiType::Gitlab => {
collection.push(Box::new(Gitlab::build(api.1, cfg).await?));
}
ApiType::Github => {
collection.push(Box::new(Github::build(api.1, cfg).await?));
}
ApiType::Forgejo => {
collection.push(Box::new(Forgejo::build(api.1, cfg).await?));
}
}
}
Ok(ApiCollection { collection })
}
pub fn collection(&self) -> &ApiCollectionInner {
self.collection.as_ref()
}
pub fn collection_mut(&mut self) -> &mut ApiCollectionInner {
self.collection.as_mut()
}
}
pub fn client_builder() -> ClientBuilder {
ClientBuilder::new().user_agent(USER_AGENT)
}
// trait iimplementations for easy use of ApiCollection follow
impl IntoIterator for ApiCollection {
fn into_iter(self) -> Self::IntoIter {
self.collection.into_iter()
}
type Item = Box<dyn ServerApi>;
type IntoIter = std::vec::IntoIter<Self::Item>;
}
impl Deref for ApiCollection {
type Target = [Box<dyn ServerApi>];
fn deref(&self) -> &Self::Target {
&self.collection[..]
}
}
impl DerefMut for ApiCollection {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.collection[..]
}
}