mirror of
https://github.com/hinto-janai/cuprate.git
synced 2025-01-22 10:34:37 +00:00
init
This commit is contained in:
parent
4f14452c77
commit
60af397a31
10 changed files with 403 additions and 0 deletions
21
Cargo.lock
generated
21
Cargo.lock
generated
|
@ -613,6 +613,17 @@ dependencies = [
|
||||||
"tower 0.5.1",
|
"tower 0.5.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cuprate-changelog"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"clap",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"ureq",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cuprate-consensus"
|
name = "cuprate-consensus"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -1250,6 +1261,15 @@ version = "1.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
@ -3371,6 +3391,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
|
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
|
"encoding_rs",
|
||||||
"flate2",
|
"flate2",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
|
@ -3,6 +3,7 @@ resolver = "3"
|
||||||
members = [
|
members = [
|
||||||
# Binaries
|
# Binaries
|
||||||
"binaries/cuprated",
|
"binaries/cuprated",
|
||||||
|
"binaries/changelog",
|
||||||
|
|
||||||
# Consensus
|
# Consensus
|
||||||
"consensus",
|
"consensus",
|
||||||
|
|
18
binaries/changelog/Cargo.toml
Normal file
18
binaries/changelog/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "cuprate-changelog"
|
||||||
|
version = "0.0.1"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Generate Cuprate release changelog templates"
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
authors = ["hinto-janai"]
|
||||||
|
repository = "https://github.com/Cuprate/cuprate/tree/main/binaries/cuprate-changelog"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { workspace = true, features = ["cargo", "help", "wrap_help", "usage", "error-context", "suggestions"] }
|
||||||
|
chrono = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
ureq = { version = "2", features = ["json", "charset"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
7
binaries/changelog/README.md
Normal file
7
binaries/changelog/README.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# `cuprate-changelog`
|
||||||
|
This binary creates a template changelog needed for `Cuprate/cuprate` releases.
|
||||||
|
|
||||||
|
For more information, run:
|
||||||
|
```bash
|
||||||
|
cargo run --bin cuprate-changelog -- --help
|
||||||
|
```
|
126
binaries/changelog/src/api.rs
Normal file
126
binaries/changelog/src/api.rs
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
//! GitHub API client.
|
||||||
|
|
||||||
|
use std::{collections::BTreeSet, time::Duration};
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use ureq::{Agent, AgentBuilder};
|
||||||
|
|
||||||
|
use crate::free::fmt_date;
|
||||||
|
|
||||||
|
pub struct CommitData {
|
||||||
|
pub commit_msgs: Vec<String>,
|
||||||
|
pub contributors: BTreeSet<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GithubApiClient {
|
||||||
|
pub start_date: DateTime<Utc>,
|
||||||
|
pub end_date: DateTime<Utc>,
|
||||||
|
agent: Agent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GithubApiClient {
|
||||||
|
const API: &str = "https://api.github.com/repos/Cuprate/cuprate";
|
||||||
|
|
||||||
|
pub fn new(start_ts: u64, end_ts: u64) -> Self {
|
||||||
|
let start_date = DateTime::from_timestamp(start_ts.try_into().unwrap(), 0).unwrap();
|
||||||
|
let end_date = DateTime::from_timestamp(end_ts.try_into().unwrap(), 0).unwrap();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
agent: AgentBuilder::new().build(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn commit_data(&self) -> CommitData {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
commit: Commit,
|
||||||
|
/// When there is no GitHub author, [`Commit::author`] will be used.
|
||||||
|
author: Option<Author>,
|
||||||
|
}
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Author {
|
||||||
|
login: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Commit {
|
||||||
|
message: String,
|
||||||
|
author: CommitAuthor,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct CommitAuthor {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut url = format!(
|
||||||
|
"{}/commits?per_page=100?since={}&until={}",
|
||||||
|
Self::API,
|
||||||
|
fmt_date(&self.start_date),
|
||||||
|
fmt_date(&self.end_date)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut responses = Vec::new();
|
||||||
|
|
||||||
|
// GitHub will split up large responses, so we must make multiple calls:
|
||||||
|
// <https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api>.
|
||||||
|
loop {
|
||||||
|
let r = self.agent.get(&url).call().unwrap();
|
||||||
|
|
||||||
|
let link = r
|
||||||
|
.header("link")
|
||||||
|
.map_or_else(String::new, ToString::to_string);
|
||||||
|
|
||||||
|
responses.extend(r.into_json::<Vec<Response>>().unwrap());
|
||||||
|
|
||||||
|
if !link.contains(r#"rel="next""#) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
url = link
|
||||||
|
.split_once("<")
|
||||||
|
.unwrap()
|
||||||
|
.1
|
||||||
|
.split_once(">")
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
std::thread::sleep(Duration::from_secs(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (mut commits, authors): (Vec<String>, Vec<String>) = responses
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| {
|
||||||
|
(
|
||||||
|
r.commit.message,
|
||||||
|
r.author.map_or(r.commit.author.name, |a| a.login),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Extract contributors.
|
||||||
|
let contributors = authors.into_iter().collect::<BTreeSet<String>>();
|
||||||
|
|
||||||
|
// Extract commit msgs.
|
||||||
|
commits.sort();
|
||||||
|
let commit_msgs = commits
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| {
|
||||||
|
// The commit message may be separated by `\n` due to
|
||||||
|
// subcommits being included in squashed GitHub PRs.
|
||||||
|
//
|
||||||
|
// This extracs the first, main message.
|
||||||
|
c.lines().next().unwrap().to_string()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
CommitData {
|
||||||
|
commit_msgs,
|
||||||
|
contributors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
62
binaries/changelog/src/changelog.rs
Normal file
62
binaries/changelog/src/changelog.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
//! Changelog generation.
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api::{CommitData, GithubApiClient},
|
||||||
|
crates::CuprateCrates,
|
||||||
|
free::fmt_date,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn generate_changelog(
|
||||||
|
crates: CuprateCrates,
|
||||||
|
api: GithubApiClient,
|
||||||
|
release_name: Option<String>,
|
||||||
|
) -> String {
|
||||||
|
// This variable will hold the final output.
|
||||||
|
let mut c = String::new();
|
||||||
|
|
||||||
|
let CommitData {
|
||||||
|
commit_msgs,
|
||||||
|
contributors,
|
||||||
|
} = api.commit_data();
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------- Initial header.
|
||||||
|
let cuprated_version = crates.crate_version("cuprated");
|
||||||
|
let release_name = release_name.unwrap_or_else(|| "NAME_OF_METAL".to_string());
|
||||||
|
let release_date = fmt_date(&Utc::now());
|
||||||
|
|
||||||
|
c += &format!("# {cuprated_version} {release_name} ({release_date})\n");
|
||||||
|
c += "DESCRIPTION ON CHANGES AND ANY NOTABLE INFORMATION RELATED TO THE RELEASE.\n\n";
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------- Temporary area for commits.
|
||||||
|
c += &format!(
|
||||||
|
"## COMMIT LIST `{}` -> `{:?}` (SORT INTO THE BELOW CATEGORIES)\n",
|
||||||
|
fmt_date(&api.start_date),
|
||||||
|
api.end_date,
|
||||||
|
);
|
||||||
|
for commit_msg in commit_msgs {
|
||||||
|
c += &format!("- {commit_msg}\n");
|
||||||
|
}
|
||||||
|
c += "\n";
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------- `cuprated` changes.
|
||||||
|
c += "## `cuprated`\n";
|
||||||
|
c += "- Example change (#PR_NUMBER)\n";
|
||||||
|
c += "\n";
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------- Library changes.
|
||||||
|
c += "## `cuprate_library`\n";
|
||||||
|
c += "- Example change (#PR_NUMBER)\n";
|
||||||
|
c += "\n";
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------- Contributors footer.
|
||||||
|
c += "## Contributors\n";
|
||||||
|
c += "Thank you to everyone who contributed to this release:\n";
|
||||||
|
|
||||||
|
for contributor in contributors {
|
||||||
|
c += &format!("- @{contributor}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
c
|
||||||
|
}
|
76
binaries/changelog/src/cli.rs
Normal file
76
binaries/changelog/src/cli.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
use std::{process::exit, time::SystemTime};
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api::GithubApiClient, changelog::generate_changelog, crates::CuprateCrates,
|
||||||
|
free::generate_cuprated_help_text,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn current_unix_timestamp() -> u64 {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CLI arguments.
|
||||||
|
#[derive(Parser, Debug, Clone)]
|
||||||
|
#[command(version, about)]
|
||||||
|
pub struct Cli {
|
||||||
|
/// List all Cuprate crates and their versions.
|
||||||
|
#[arg(long)]
|
||||||
|
pub list_crates: bool,
|
||||||
|
|
||||||
|
/// The start UNIX timestamp of the changelog.
|
||||||
|
#[arg(long, default_value_t)]
|
||||||
|
pub start_timestamp: u64,
|
||||||
|
|
||||||
|
/// The end UNIX timestamp of the changelog.
|
||||||
|
#[arg(long, default_value_t = current_unix_timestamp())]
|
||||||
|
pub end_timestamp: u64,
|
||||||
|
|
||||||
|
/// The release's code name (should be a metal).
|
||||||
|
#[arg(long)]
|
||||||
|
pub release_name: Option<String>,
|
||||||
|
|
||||||
|
/// Generate and output the changelog to stdout.
|
||||||
|
#[arg(long)]
|
||||||
|
pub changelog: bool,
|
||||||
|
|
||||||
|
/// Output `cuprated --help` to stdout.
|
||||||
|
#[arg(long)]
|
||||||
|
pub cuprated_help: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cli {
|
||||||
|
/// Complete any quick requests asked for in [`Cli`].
|
||||||
|
pub fn do_quick_requests(self) -> Self {
|
||||||
|
let crates = CuprateCrates::new();
|
||||||
|
let api = GithubApiClient::new(self.start_timestamp, self.end_timestamp);
|
||||||
|
|
||||||
|
if self.list_crates {
|
||||||
|
for pkg in crates.packages {
|
||||||
|
println!("{} {}", pkg.version, pkg.name);
|
||||||
|
}
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.changelog {
|
||||||
|
println!("{}", generate_changelog(crates, api, self.release_name));
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.cuprated_help {
|
||||||
|
println!("{}", generate_cuprated_help_text());
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init() -> Self {
|
||||||
|
let this = Self::parse();
|
||||||
|
this.do_quick_requests()
|
||||||
|
}
|
||||||
|
}
|
38
binaries/changelog/src/crates.rs
Normal file
38
binaries/changelog/src/crates.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
//! TODO
|
||||||
|
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// [`CargoMetadata::packages`]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)]
|
||||||
|
pub struct Package {
|
||||||
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)]
|
||||||
|
pub struct CuprateCrates {
|
||||||
|
pub packages: Vec<Package>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CuprateCrates {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let output = Command::new("cargo")
|
||||||
|
.args(["metadata", "--no-deps"])
|
||||||
|
.output()
|
||||||
|
.unwrap()
|
||||||
|
.stdout;
|
||||||
|
|
||||||
|
serde_json::from_slice(&output).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn crate_version(&self, crate_name: &str) -> &str {
|
||||||
|
&self
|
||||||
|
.packages
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.name == crate_name)
|
||||||
|
.unwrap()
|
||||||
|
.version
|
||||||
|
}
|
||||||
|
}
|
38
binaries/changelog/src/free.rs
Normal file
38
binaries/changelog/src/free.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
//! Free functions.
|
||||||
|
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
use crate::crates::CuprateCrates;
|
||||||
|
|
||||||
|
/// Assert we are at `Cuprate/cuprate`.
|
||||||
|
///
|
||||||
|
/// This binary relys on this.
|
||||||
|
pub fn assert_repo_root() {
|
||||||
|
let path = std::env::current_dir().unwrap();
|
||||||
|
|
||||||
|
// Check path.
|
||||||
|
assert!(
|
||||||
|
path.ends_with("Cuprate/cuprate"),
|
||||||
|
"This binary must be ran at the repo root."
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sanity check cargo.
|
||||||
|
CuprateCrates::new().crate_version("cuprated");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fmt_date(date: &DateTime<Utc>) -> String {
|
||||||
|
date.format("%Y-%m-%d").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_cuprated_help_text() -> String {
|
||||||
|
String::from_utf8(
|
||||||
|
Command::new("cargo")
|
||||||
|
.args(["run", "--bin", "cuprated", "--", "--help"])
|
||||||
|
.output()
|
||||||
|
.unwrap()
|
||||||
|
.stdout,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
16
binaries/changelog/src/main.rs
Normal file
16
binaries/changelog/src/main.rs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
#![allow(unreachable_pub, reason = "Binary")]
|
||||||
|
#![allow(clippy::needless_pass_by_value, reason = "Efficiency doesn't matter")]
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
mod changelog;
|
||||||
|
mod cli;
|
||||||
|
mod crates;
|
||||||
|
mod free;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
free::assert_repo_root();
|
||||||
|
|
||||||
|
cli::Cli::init();
|
||||||
|
}
|
Loading…
Reference in a new issue