From 547e509683007b9e0c149d847ac685f3aa770de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 17 Sep 2024 04:44:56 +0200 Subject: [PATCH] Implement a `changelog-generator` tool and example --- CHANGELOG.md | 5 +- examples/changelog/Cargo.toml | 23 ++ examples/changelog/fonts/changelog-icons.ttf | Bin 0 -> 5764 bytes examples/changelog/src/changelog.rs | 354 ++++++++++++++++++ examples/changelog/src/icon.rs | 10 + examples/changelog/src/main.rs | 368 +++++++++++++++++++ 6 files changed, 759 insertions(+), 1 deletion(-) create mode 100644 examples/changelog/Cargo.toml create mode 100644 examples/changelog/fonts/changelog-icons.ttf create mode 100644 examples/changelog/src/changelog.rs create mode 100644 examples/changelog/src/icon.rs create mode 100644 examples/changelog/src/main.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 16a69a7a..e8ac8d68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `fetch_position` command in `window` module. [#2280](https://github.com/iced-rs/iced/pull/2280) -Many thanks to... +### Fixed +- Fix `block_on` in `iced_wgpu` hanging Wasm builds. [#2313](https://github.com/iced-rs/iced/pull/2313) +Many thanks to... +- @hecrj - @n1ght-hunter ## [0.12.1] - 2024-02-22 diff --git a/examples/changelog/Cargo.toml b/examples/changelog/Cargo.toml new file mode 100644 index 00000000..6f914bce --- /dev/null +++ b/examples/changelog/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "changelog" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced.workspace = true +iced.features = ["tokio", "markdown", "highlighter", "debug"] + +log.workspace = true +thiserror.workspace = true +tokio.features = ["fs", "process"] +tokio.workspace = true + +serde = "1" +webbrowser = "1" + +[dependencies.reqwest] +version = "0.12" +default-features = false +features = ["json", "rustls-tls"] diff --git a/examples/changelog/fonts/changelog-icons.ttf b/examples/changelog/fonts/changelog-icons.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a0f32553f25efedf1acc7a9cf56e65b21a170310 GIT binary patch literal 5764 zcmZQzWME+6XJ}wxW+-qE4s}xKR;^-SVEDtpz!2getZ!te?75eLfiZxAfgvF|H?iQQ z#_m-N42&BX7#ItZ%Ssd&z<~V>0|NtJT0wellHSiM1_t&!3=Dic>50V!3=9kc3=Hf? z7#J8h(sL@)v^f6fF)%QL^zF+?O-#|$2rpz{V1B^Bz+jer?V3mvv91M&MY*0}a<{-ui46F=1_qG*j3HomGBEftFoRh-49pB%3=E7R3@i*vU^Oft@fQsL|1&T^NCuGG!Tw_e zxf@~r$Yk+=y)6SK!Jz_59w5xX$iT$F@IQ-b4|5QMAcG`BhW7^EfCvW-P;|2uFfg+) zvM@6?FfcN*62c4&LJard z=9t1$HzNZ-gDF&;iGiKL1x{m<>!^8=H%q-CFkcRXC&sOr{?6R>t-hB=M@K~rkCa<7NLoval=xJiZk=` z6b$tY&}7_G^HPfvOHxxnHW!zr8 zgEK=uLjgl2LlHwJLpnnSLkWWdg9d{$gC>IlgAs!PgAs!Xg91YmLnVU(Lq1ehCPN-W zB0~;?0z)E0DMJZE23V$;L4m=6L4hHSA)g_Sp@booA%`J{A)i5yA(oE zF&Gdq(VZccArGuC5$y65xSxv|${5laj2KE7N*L0>zAFOzMuEW%Y(B!D3Je7dAh|4t zREA`R5(YhRXqGS(Fjz6@Gw94eD0Na+nssvCHmlQ%E~MQmX31}RqDz~e0Ky+ID7O3piRhX6w|NW~6ThNQ^Il*Gsl zjM|ZrP#;7pq-+p#R(6Wi-N34&;Ht2JMKv)+VFSB!K*R=iWv2}s%1+XXijf-_6LdGQ zfrEPkv$jIYM)o8p1&}1jyV4NngUAgG39iW-ShW-tHgGsAbSZRgVAS4dz^b}|MJ+HQ zAwW7YC^AAhQZZ6tgF|q{21adZP`E&yqPu}zX9K6R_9g};^bp+c6HKH zgeM$s9n^%x12KhBTX_S6^9B~x1O?YFa{ME|Dr5cvF=tbTwIfq7U z6cTdYz?kT?K~Ni%I&I_!5duuAP8%4-lod8Gs|G}D1m(jGEUHdjT?!y;1+)~E zH!vo`*wPSrkQOG@#0>&kijf=mo!vJGIJ+loU_y;7g$-Dva3ceQu(HbrM&}I-cAFR( z8Nu0JREJ?B1Cwh=#0DW{Ck59He9BH6c$GIWBseJ~Y?qK=WDsN!W^i(H0)>ExveO2} z#El{x+8esHrIjLeH;CzMWDwQX-5{>Bkr6~o=xk&H(ULkFnL)Ia&PEmxEv>VW6-3ME zY-9t`vN{{tLA0FCMh*}yud|U8L~H9P!@_?9TY`5;gtCI(2F6%WZqePKgCr>imn?)x z>LN+1!6l0zk_tKu8~C+x`f~$g;#P=PLH14N5v289}tN&PFB>t)jD$ z8APknJ2_kVsH) zR^Gsv;0!99L77kwlx7WdHt1__(AU!4V5p;@;I4opHkA`CltBeMUKP@ck-8gb&W8H1jnK=Qt;qXW1^#A3U;}v4g*w;D`qjVfl(W4+5?F*h;0Ga zTP}9W78^|%ML~Ww(^0TdaL3`p4GhkS7P=eEb@aq-bT?QKQo4cBIoU#YgQdNPZiyDs${V}60gR)d6m;szdNCrC;KC1+&?8wEXO3#fZ_?8OWhf z-@t>wQ)dH%C?wl&VgTiEVZ9AzTDlv&aEN>BY-C^*5#C^~rMtmLXM?4d?gn2_D7q>u z=qb2$DJQ~0%TEWCpf~tx>u&JZ*~Gx$rmednKxY#JBZv{GvxyNb5~Q<<5iAl6Qsb_z zyCDRm2E+&jsR1#cFg6kUB6c4x|puiU-;0s;#>r0c0nLkqELA#7F|!31TFJ)Ocv?Zb$*C0WnfR zYCw!MkQxvp9b_Mai?;5D43K?bRwl?kFe?jWADER5QpX5VmjhA{_wLzF590QCcxF(n)&*N9qPC0Afa6x4GfGd4jsuMV3iCmksz>, + added: Vec, + changed: Vec, + fixed: Vec, + removed: Vec, + authors: Vec, +} + +impl Changelog { + pub fn new() -> Self { + Self { + ids: Vec::new(), + added: Vec::new(), + changed: Vec::new(), + fixed: Vec::new(), + removed: Vec::new(), + authors: Vec::new(), + } + } + + pub async fn list() -> Result<(Self, Vec), Error> { + let mut changelog = Self::new(); + + { + let markdown = fs::read_to_string("CHANGELOG.md").await?; + + if let Some(unreleased) = markdown.split("\n## ").nth(1) { + let sections = unreleased.split("\n\n"); + + for section in sections { + if section.starts_with("Many thanks to...") { + for author in section.lines().skip(1) { + let author = author.trim_start_matches("- @"); + + if author.is_empty() { + continue; + } + + changelog.authors.push(author.to_owned()); + } + + continue; + } + + let Some((_, rest)) = section.split_once("### ") else { + continue; + }; + + let Some((name, rest)) = rest.split_once("\n") else { + continue; + }; + + let category = match name { + "Added" => Category::Added, + "Fixed" => Category::Fixed, + "Changed" => Category::Changed, + "Removed" => Category::Removed, + _ => continue, + }; + + for entry in rest.lines() { + let Some((_, id)) = entry.split_once('#') else { + continue; + }; + + let Some((id, _)) = id.split_once(']') else { + continue; + }; + + let Ok(id): Result = id.parse() else { + continue; + }; + + changelog.ids.push(id); + + let target = match category { + Category::Added => &mut changelog.added, + Category::Changed => &mut changelog.added, + Category::Fixed => &mut changelog.fixed, + Category::Removed => &mut changelog.removed, + }; + + target.push(entry.to_owned()); + } + } + } + } + + let mut candidates = Candidate::list().await?; + + for reviewed_entry in changelog.entries() { + candidates.retain(|candidate| candidate.id != reviewed_entry); + } + + Ok((changelog, candidates)) + } + + pub fn len(&self) -> usize { + self.ids.len() + } + + pub fn entries(&self) -> impl Iterator + '_ { + self.ids.iter().copied() + } + + pub fn push(&mut self, entry: Entry) { + self.ids.push(entry.id); + + let item = format!( + "- {title}. [#{id}](https://github.com/iced-rs/iced/pull/{id})", + title = entry.title, + id = entry.id + ); + + let target = match entry.category { + Category::Added => &mut self.added, + Category::Changed => &mut self.added, + Category::Fixed => &mut self.fixed, + Category::Removed => &mut self.removed, + }; + + target.push(item); + + if !self.authors.contains(&entry.author) { + self.authors.push(entry.author); + self.authors.sort_by_key(|author| author.to_lowercase()); + } + } +} + +impl fmt::Display for Changelog { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn section(category: Category, entries: &[String]) -> String { + if entries.is_empty() { + return String::new(); + } + + format!("### {category}\n{list}\n", list = entries.join("\n")) + } + + fn thank_you<'a>(authors: impl IntoIterator) -> String { + let mut list = String::new(); + + for author in authors { + list.push_str(&format!("- @{author}\n")); + } + + format!("Many thanks to...\n{list}") + } + + let changelog = [ + section(Category::Added, &self.added), + section(Category::Changed, &self.changed), + section(Category::Fixed, &self.fixed), + section(Category::Removed, &self.removed), + thank_you(self.authors.iter().map(String::as_str)), + ] + .into_iter() + .filter(|section| !section.is_empty()) + .collect::>() + .join("\n"); + + f.write_str(&changelog) + } +} + +#[derive(Debug, Clone)] +pub struct Entry { + pub id: u64, + pub title: String, + pub category: Category, + pub author: String, +} + +impl Entry { + pub fn new( + title: &str, + category: Category, + pull_request: &PullRequest, + ) -> Option { + let title = title.strip_suffix(".").unwrap_or(title); + + if title.is_empty() { + return None; + }; + + Some(Self { + id: pull_request.id, + title: title.to_owned(), + category, + author: pull_request.author.clone(), + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Category { + Added, + Changed, + Fixed, + Removed, +} + +impl Category { + pub const ALL: &'static [Self] = + &[Self::Added, Self::Changed, Self::Fixed, Self::Removed]; + + pub fn guess(label: &str) -> Option { + Some(match label { + "feature" | "addition" => Self::Added, + "change" => Self::Changed, + "bug" | "fix" => Self::Fixed, + _ => None?, + }) + } +} + +impl fmt::Display for Category { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Category::Added => "Added", + Category::Changed => "Changed", + Category::Fixed => "Fixed", + Category::Removed => "Removed", + }) + } +} + +#[derive(Debug, Clone)] +pub struct Candidate { + pub id: u64, +} + +#[derive(Debug, Clone)] +pub struct PullRequest { + pub id: u64, + pub title: String, + pub description: String, + pub labels: Vec, + pub author: String, +} + +impl Candidate { + pub async fn list() -> Result, Error> { + let output = process::Command::new("git") + .args([ + "log", + "--oneline", + "--grep", + "#[0-9]*", + "origin/latest..HEAD", + ]) + .output() + .await?; + + let log = String::from_utf8_lossy(&output.stdout); + + Ok(log + .lines() + .filter(|title| !title.is_empty()) + .filter_map(|title| { + let (_, pull_request) = title.split_once("#")?; + let (pull_request, _) = pull_request.split_once([')', ' '])?; + + Some(Candidate { + id: pull_request.parse().ok()?, + }) + }) + .collect()) + } + + pub async fn fetch(self) -> Result { + let request = reqwest::Client::new() + .request( + reqwest::Method::GET, + format!( + "https://api.github.com/repos/iced-rs/iced/pulls/{}", + self.id + ), + ) + .header("User-Agent", "iced changelog generator") + .header( + "Authorization", + format!( + "Bearer {}", + env::var("GITHUB_TOKEN") + .map_err(|_| Error::GitHubTokenNotFound)? + ), + ); + + #[derive(Deserialize)] + struct Schema { + title: String, + body: String, + user: User, + labels: Vec