This commit is contained in:
hinto.janai 2025-01-19 11:22:38 -05:00
parent 4f14452c77
commit 60af397a31
No known key found for this signature in database
GPG key ID: D47CE05FA175A499
10 changed files with 403 additions and 0 deletions

21
Cargo.lock generated
View file

@ -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",

View file

@ -3,6 +3,7 @@ resolver = "3"
members = [ members = [
# Binaries # Binaries
"binaries/cuprated", "binaries/cuprated",
"binaries/changelog",
# Consensus # Consensus
"consensus", "consensus",

View 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

View 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
```

View 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,
}
}
}

View 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
}

View 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()
}
}

View 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
}
}

View 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()
}

View 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();
}