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 zcmd^DU2Ggz6+ScjW5;&<6aOUbrr~bJb{y~Q#&Le?rf%xa?AlG;pN-?BJ1M0z-d+Es z>)q{o6OsZY&=%4yMU{{$9(dq^7Z5^s94HACR3s#j-~rwds>Dkw0dGhk0pU9{v##wX zjo_7ucJ6o2`R=*roO|xQeA{$>K_>2@&|M~mYlUJB{10 z>+Ld8NTC1o<)TwKIP&aE=zoCz@nr~FCPQb5!h6skTwbkTj(!J-!jpKlmMVFNUhw*9 zyrZkm@)7|J%?Xsn&|sSKfX2U84S5 z=zohi_BI-$lcc7npPZ-8l2b3!{S=0rAJ%WiN@-=uq1Hg7K*w4I5{05oerQg4n<7N& z%j$cOea&x`59qL>LQ0Vkq<46-i+yEVEmp^Ojo_w+@z`2OKzJx7lp*&RO+KXBy1BfF2Nm-dd1?)}&A_&urI82R@% z_0r(C2g~YTA#L3^GO|zo``*!a#tVgl9|1=}Z*;sk|3cT~->56(1@@=TH#W7mFNAI( zLhLb)*LyI=t3&enctf(~QCZ{FIE#5-GDF4w@olKs`D64JWHX(9hKY93A2;kA3r6s` zE;<&-RcfJhfJ2ba1~?3SKEM%bry8&*abIPv*+)l&l17@XqrM9xPTo)teK`}ly zepD=65fzA5%1%i*7wgNF+M1XWiVMO1@HFn=39|EIGAp z(rw*y#oF3Rr7RAo#sTT`8^V+4Q@p^qxEY>QkVmg>pRBM$B#e6-rT(4J8 z96Gew7c!|rRk}hoBzK9Hkyb$?kRC)kj-?!@qd*H_2rzxt3YE#B5+n{?#E4~2vxfc@ zev9apAuWPY0!~pL?^V2)!7pPL)>P0YF(>QFv_^A~F2Py}oZ3COpKR$IEY)Cf#j`Bv zFgzH$hs7C;DSP7_&+`J}@MB)1OYmzP(mEpJ=xSaxf~GzDAH^w1-}z2{C(vI#18a z=$Z35c{C2>TP(>9V)(WS5#lTy{SxE4>! zkS+?>Lw)^Zn6lRtwr#j-pJ}*5Ayca6{AD3KGyu~n$Z+=Zx~i%$Bjd%rah9%k_AAC- z0jL@4y-F{1nxt&5Xsr(@%uLF#E<*{~Z{~PDIbfOrAtnk!zMqxh;q&Vw%1$$r&&X&d z7nh+SYwo!m$gxIF$ZQs(DJv%A1fvPd60XmNM&~!m@%0 zEKxu_nqehnL>Cz$!$S^YYB94p*`gV;RWq;x7-A=7)Jr!qAqsBGf+1MR;l_OT`7O!L zGvB(fIbUJ5eW6qqF)2z59^S;{}l-1}B?dVrexe?N71uF2m&@Mq%(qXqyLha{X(@ z^_?9Q%3z6NTHG2%5qIgXs%B)tNhf8uju3>9UFH)U830yDc5`nIy>4&4dSIyAONu}a z@^D=Cn6_xxLiQkSNx4g(ozJ=9LfRUXJB!-or0msaPv>UO_`+Bm@?KBgr@OSvJezZO z?J}j}7_uk9TMngVxLy3}#!D&#Sc}k5Hs|s_M7oRyRv6QEkH$3&Yhu&`>v;%vGHWeF zG6Vl-AlUhQBWt>6-Dvtp{0MB z)Juk8a0dFIQyjOiH6h>W3x=1gdOuzw;0 z<(QP?%_Vj_qZzI^d3e-~DE*ln4nOs25Y{Z%*T%joIZE-mgO|? zL+{>7pVTJYqe?%=dK~#c0CyZ?W3xCDN%^3j+C7<+6ZfK`u=Ailge9VZA(0X@d?X;X zry7kJZ3ahB4i_IzUED|$iqhYQ44=T+H6V9`7RGTp5R(nqe0VEO zqSy+!y+PL*Tch+`z?b{+BO{8m>(W!)3%IawYP3p>?|eW-%4TA zE@(1>OBIJ**mPpRwuSRy`xpnV;r!R8or#!+8Kw~@8gCZZyTc@KgpcGX4iPLy1O& zOzZODL<8l=HP*mRzngDZ(3Bj5iy1FuF5_W8^A0}H3_o-(XAL_og`MXIG_AXBxNErj zy~_XZN_yr_1@^yGG3T4lu}N(r7T;7aaVzk11_$toL^CUoqxWzk&TGbb38L9Z!6~Gm z-{1E5I>8R_P08cfxLQ%mH|kGt8NRYMwa^&N7F=Ip#2UR=;cf@A^y`W*I}hZ` zo@1WE+D|jjL(enML!V)uhhE@0GoaZ#Cj*D)WU#<p)f?)5x-U-2Q+zv@G#U&F{p zeS2^CkO>VRGT}{qz1{n=mC@LGSPf, + 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