a basic changelog interface
cargo devel CI / cargo CI (push) Successful in 4m21s Details

This commit is contained in:
Christoph J. Scherr 2024-01-27 23:26:49 +01:00
parent e75f072cb0
commit 0d2ac7e163
Signed by: PlexSheep
GPG Key ID: 7CDD0B14851A08EF
9 changed files with 202 additions and 53 deletions

View File

@ -1,3 +1,4 @@
---
changelog: changelog:
enable: true enable: true
git-log: true git-log: true
@ -5,10 +6,10 @@ changelog:
uses: uses:
cargo: cargo:
publish: true publish: true
# tokens are loaded from ~/.cargo/config.toml # tokens are loaded from ~/.cargo/config.toml
registries: registries:
- crates.io - crates.io
- example.com - cscherr
api: api:
github: github:
@ -19,7 +20,7 @@ api:
pass: token_superimportantsecret pass: token_superimportantsecret
myserv: myserv:
type: gitea type: gitea
endpoint: https://git.example.com endpoint: https://git.cscherr.de
auth: auth:
user: myUserName user: myUserName
pass: importantsecrettoken pass: importantsecrettoken

View File

@ -1,6 +1,6 @@
[package] [package]
name = "autocrate" name = "autocrate"
version = "0.1.0-prealpha.0" version = "0.1.0-prealpha.1"
edition = "2021" edition = "2021"
publish = true publish = true
authors = ["Christoph J. Scherr <software@cscherr.de>"] authors = ["Christoph J. Scherr <software@cscherr.de>"]
@ -21,8 +21,8 @@ keywords = [
[dependencies] [dependencies]
anyhow = "1.0.79" anyhow = "1.0.79"
clap = { version = "4.4.18", features = ["derive"] } cargo = "0.76.0"
clap-num = "1.1.1" clap = { version = "4.4.18", features = ["derive", "help"] }
clap-verbosity-flag = "2.1.2" clap-verbosity-flag = "2.1.2"
git2 = "0.18.1" git2 = "0.18.1"
libpt = { version = "0.3.11", features = ["log"] } libpt = { version = "0.3.11", features = ["log"] }

View File

@ -18,7 +18,6 @@ Autocrate streamlines the release process, allowing developers to focus on
their work. Although initially built for Gitea, we plan to extend support their work. Although initially built for Gitea, we plan to extend support
for additional platforms such as GitHub and GitLab. for additional platforms such as GitHub and GitLab.
* [Original Repository](https://git.cscherr.de/PlexSheep/Autocrate) * [Original Repository](https://git.cscherr.de/PlexSheep/Autocrate)
* [GitHub Mirror](https://github.com/PlexSheep/Autocrate) * [GitHub Mirror](https://github.com/PlexSheep/Autocrate)
* [crates.io](https://crates.io/crates/autocrate) * [crates.io](https://crates.io/crates/autocrate)
@ -83,6 +82,7 @@ repository. It should contain the following parameters (replace the placeholders
| `api.NAME.auth` | `pass` | a string | A secret for authentication o the server, probably a token | | `api.NAME.auth` | `pass` | a string | A secret for authentication o the server, probably a token |
An example `.autocrate.yaml` could look like this: An example `.autocrate.yaml` could look like this:
```yaml ```yaml
changelog: changelog:
enable: true enable: true

53
src/changelog/mod.rs Normal file
View File

@ -0,0 +1,53 @@
use std::fmt::Display;
use crate::{
config::{cli::Commands, Config},
error::*,
};
/// Represents a changelog that is currently under construction.
#[derive(Clone, Debug)]
pub struct Changelog {
message: Option<String>,
git_log: Option<String>,
}
impl Changelog {
pub fn build(cfg: &Config) -> Result<Self> {
if !cfg.yaml.changelog.enable {
return Err(ConfigError::IsDisabledButUsed("changelog").into());
}
let message: Option<String> = match cfg.cli.command.clone() {
Commands::Changelog { message } => match message {
Some(msgs) => Some(msgs.concat()),
None => None,
},
};
let git_log = Self::make_git_log(cfg)?;
Ok(Changelog {
message,
git_log,
})
}
fn make_git_log(cfg: &Config) -> Result<Option<String>> {
if !cfg.yaml.changelog.enable {
return Ok(None);
}
Ok(Some(format!("todo")))
}
}
impl Display for Changelog {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut full: String = String::new();
full += "Changelog";
if self.message.is_some() {
full += format!("\n\n{}", self.message.clone().unwrap()).as_str();
}
if self.git_log.is_some() {
full += format!("\n\n{}", self.git_log.clone().unwrap()).as_str();
}
write!(f, "{full}")
}
}

View File

@ -1,39 +1,68 @@
use std::fmt::Display;
use libpt::log::{Level, Logger}; use libpt::log::{Level, Logger};
use clap::Parser; use clap::{Parser, Subcommand};
use clap_verbosity_flag::{InfoLevel, Verbosity}; use clap_verbosity_flag::{InfoLevel, Verbosity};
/// short about section displayed in help
const ABOUT_ROOT: &'static str = r##"
Release Manager for Your Projects on Gitea, GitHub, and GitLab.
"##;
/// longer about section displayed in help, is combined with [the short help](ABOUT_ROOT)
static LONG_ABOUT_ROOT: &'static str = r##""##;
#[derive(Debug, Clone, Parser)] #[derive(Debug, Clone, Parser)]
#[command( #[command(
author, author,
version, version,
about = ABOUT_ROOT, about,
long_about = format!("{}{}", ABOUT_ROOT ,LONG_ABOUT_ROOT), long_about,
help_template = help_template = r#"{about-section}
r#"{about-section}
{usage-heading} {usage} {usage-heading} {usage}
{all-args}{tab} {all-args}{tab}
autocrate: {version} autocrate: {version}
Author: {author-with-newline} Author: {author-with-newline}
"# "#
)] )]
/// Release Manager for Your Projects on Gitea, GitHub, and GitLab.
pub struct Cli { pub struct Cli {
// clap_verbosity_flag seems to make this a global option implicitly // clap_verbosity_flag seems to make this a global option implicitly
/// set a verbosity, multiple allowed (f.e. -vvv) /// set a verbosity, multiple allowed (f.e. -vvv)
#[command(flatten)] #[command(flatten)]
pub(crate) verbose: Verbosity<InfoLevel>, pub verbose: Verbosity<InfoLevel>,
/// show additional logging meta data /// show additional logging meta data
#[arg(long)] #[arg(long)]
pub(crate) meta: bool, pub meta: bool,
/// the subcommands are part of this enum
#[command(subcommand)]
pub command: Commands,
}
#[derive(Debug, Clone, Subcommand)]
pub enum Commands {
Changelog {
// FIXME: allow taking a message like this:
// `autocrate changelog -m arg1 arg2 arg3`
// -> msg="arg1 arg2 arg3"
// Instead of only
// `autocrate changelog -m "arg1 arg2 arg3"`
// -> msg="arg1 arg2 arg3"
//
// TODO:
// Perhaps open the $EDITOR of the user if
// no message is provided, like git does
#[arg(short, long)]
message: Option<Vec<String>>,
},
}
impl Display for Commands {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Changelog { .. } => "Changelog",
}
)
}
} }
impl Cli { impl Cli {

View File

@ -10,54 +10,84 @@ use crate::error::*;
pub mod cli; pub mod cli;
use cli::Cli; use cli::Cli;
pub trait YamlConfigSection: Debug + Clone + for<'a> Deserialize<'a> {
fn check(&self) -> Result<()>;
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Changelog { pub struct Changelog {
enable: bool, pub enable: bool,
#[serde(alias = "git-log")] #[serde(alias = "git-log")]
git_log: bool, pub git_log: bool,
}
impl YamlConfigSection for Changelog {
fn check(&self) -> Result<()> {
Ok(())
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct UseCargo {
publish: bool,
registries: Vec<String>,
}
impl YamlConfigSection for UseCargo {
fn check(&self) -> Result<()> {
Ok(())
}
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Uses { pub struct Uses {
cargo: Option<Cargo>, cargo: UseCargo,
} }
impl YamlConfigSection for Uses {
#[derive(Debug, Clone, Deserialize)] fn check(&self) -> Result<()> {
pub struct Cargo { self.cargo.check()?;
publish: bool, Ok(())
registries: Vec<String>, }
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct ApiAuth { pub struct ApiAuth {
user: String, pub user: String,
pass: Option<String>, pub pass: Option<String>,
pass_file: Option<PathBuf>, pub pass_file: Option<PathBuf>,
} }
impl YamlConfigSection for ApiAuth {
impl ApiAuth { fn check(&self) -> Result<()> {
pub fn check(&self) -> Result<()> {
if self.pass.is_some() && self.pass_file.is_some() { if self.pass.is_some() && self.pass_file.is_some() {
let err = ConfigError::YamlApiAuthBothPass(self.clone()).into(); let err = ConfigError::YamlApiAuthBothPass(self.clone()).into();
error!("{err}"); error!("{err}");
return Err(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());
}
}
Ok(()) Ok(())
} }
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Api { pub struct Api {
r#type: ApiType, pub r#type: ApiType,
endpoint: Url, pub endpoint: Url,
/// May be left empty if the Api does not need auth or the auth is part of the /// May be left empty if the Api does not need auth or the auth is part of the
/// [endpoint](Api::endpoint) [Url]. /// [endpoint](Api::endpoint) [Url].
auth: Option<ApiAuth>, pub auth: Option<ApiAuth>,
} }
impl Api { impl YamlConfigSection for Api {
pub fn check(&self) -> Result<()> { fn check(&self) -> Result<()> {
if let Some(auth) = &self.auth { self.r#type.check()?;
auth.check()?; match self.endpoint.socket_addrs(|| None) {
Ok(_) => (),
Err(err) => return Err(err.into()),
}
if self.auth.is_some() {
self.auth.clone().unwrap().check()?;
} }
Ok(()) Ok(())
} }
@ -72,6 +102,11 @@ pub enum ApiType {
#[serde(alias = "github", alias = "GitHub")] #[serde(alias = "github", alias = "GitHub")]
Github, Github,
} }
impl YamlConfigSection for ApiType {
fn check(&self) -> Result<()> {
Ok(())
}
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct YamlConfig { pub struct YamlConfig {
@ -79,6 +114,16 @@ pub struct YamlConfig {
pub uses: Uses, pub uses: Uses,
pub api: HashMap<String, Api>, pub api: HashMap<String, Api>,
} }
impl YamlConfigSection for YamlConfig {
fn check(&self) -> Result<()> {
self.changelog.check()?;
self.uses.check()?;
for api in self.api.values() {
api.check()?;
}
Ok(())
}
}
impl YamlConfig { impl YamlConfig {
/// check if the built configuration is valid /// check if the built configuration is valid
@ -92,6 +137,7 @@ impl YamlConfig {
pub struct Config { pub struct Config {
pub yaml: YamlConfig, pub yaml: YamlConfig,
pub cli: Cli,
pub repo: git2::Repository, pub repo: git2::Repository,
pub path: PathBuf, pub path: PathBuf,
} }
@ -110,7 +156,7 @@ impl Debug for Config {
} }
impl 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() { let repo = match git2::Repository::open_from_env() {
Ok(repo) => repo, Ok(repo) => repo,
Err(_err) => { Err(_err) => {
@ -147,6 +193,11 @@ impl Config {
yaml.check()?; yaml.check()?;
debug!("built and checked yaml config"); debug!("built and checked yaml config");
Ok(Config { yaml, repo, path }) Ok(Config {
yaml,
repo,
path,
cli,
})
} }
} }

View File

@ -1,3 +1,5 @@
use std::path::PathBuf;
use anyhow; use anyhow;
use thiserror::Error; use thiserror::Error;
@ -28,4 +30,8 @@ pub enum ConfigError {
YamlFileIsNotFile, YamlFileIsNotFile,
#[error("api {0:?} provides both a `pass` and a `pass_file`")] #[error("api {0:?} provides both a `pass` and a `pass_file`")]
YamlApiAuthBothPass(ApiAuth), YamlApiAuthBothPass(ApiAuth),
#[error("{0} has 'enabled = false' in the yaml config")]
IsDisabledButUsed(&'static str),
#[error("password provided as file, but does not exist: {0}")]
PassFileDoesNotExist(PathBuf),
} }

View File

@ -1,2 +1,3 @@
pub mod config; pub mod config;
pub mod error; pub mod error;
pub mod changelog;

View File

@ -1,12 +1,20 @@
use anyhow::Result; use autocrate::{
changelog::*,
mod config; config::{
mod error; cli::{Cli, Commands},
use config::{cli::Cli, Config}; Config,
},
error::*,
};
fn main() -> Result<()> { fn main() -> Result<()> {
let cli = Cli::cli_parse(); let cli = Cli::cli_parse();
let _config = Config::load(cli.clone())?; let cfg = Config::load(cli.clone())?;
todo!() match cli.command {
Commands::Changelog { .. } => {
println!("{}", Changelog::build(&cfg)?.to_string());
Ok(())
}
}
} }