From 1be278e60cca7433225bd2502afbd6e1200fb976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 00:21:56 +0200 Subject: [PATCH] Save `CHANGELOG.md` after each review in `changelog` tool --- CHANGELOG.md | 1 - examples/changelog/fonts/changelog-icons.ttf | Bin 5764 -> 0 bytes examples/changelog/src/changelog.rs | 69 +++++++++---- examples/changelog/src/main.rs | 96 ++++++++++--------- 4 files changed, 99 insertions(+), 67 deletions(-) delete mode 100644 examples/changelog/fonts/changelog-icons.ttf diff --git a/CHANGELOG.md b/CHANGELOG.md index e8ac8d68..2a381e21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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/fonts/changelog-icons.ttf b/examples/changelog/fonts/changelog-icons.ttf deleted file mode 100644 index a0f32553f25efedf1acc7a9cf56e65b21a170310..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5764 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 Result<(Self, Vec), Error> { + pub async fn list() -> Result<(Self, Vec), Error> { let mut changelog = Self::new(); { @@ -97,7 +97,7 @@ impl Changelog { } } - let mut candidates = Candidate::list().await?; + let mut candidates = Contribution::list().await?; for reviewed_entry in changelog.entries() { candidates.retain(|candidate| candidate.id != reviewed_entry); @@ -106,6 +106,30 @@ impl Changelog { Ok((changelog, candidates)) } + pub async fn save(self) -> Result<(), Error> { + let markdown = fs::read_to_string("CHANGELOG.md").await?; + + let Some((header, rest)) = markdown.split_once("\n## ") else { + return Err(Error::InvalidFormat); + }; + + let Some((_unreleased, rest)) = rest.split_once("\n## ") else { + return Err(Error::InvalidFormat); + }; + + let unreleased = format!( + "\n## [Unreleased]\n{changelog}", + changelog = self.to_string() + ); + + let rest = format!("\n## {rest}"); + + let changelog = [header, &unreleased, &rest].concat(); + fs::write("CHANGELOG.md", changelog).await?; + + Ok(()) + } + pub fn len(&self) -> usize { self.ids.len() } @@ -132,7 +156,7 @@ impl Changelog { target.push(item); - if !self.authors.contains(&entry.author) { + if entry.author != "hecrj" && !self.authors.contains(&entry.author) { self.authors.push(entry.author); self.authors.sort_by_key(|author| author.to_lowercase()); } @@ -238,21 +262,12 @@ impl fmt::Display for Category { } #[derive(Debug, Clone)] -pub struct Candidate { +pub struct Contribution { 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> { +impl Contribution { + pub async fn list() -> Result, Error> { let output = process::Command::new("git") .args([ "log", @@ -273,20 +288,31 @@ impl Candidate { let (_, pull_request) = title.split_once("#")?; let (pull_request, _) = pull_request.split_once([')', ' '])?; - Some(Candidate { + Some(Contribution { id: pull_request.parse().ok()?, }) }) .collect()) } +} - pub async fn fetch(self) -> Result { +#[derive(Debug, Clone)] +pub struct PullRequest { + pub id: u64, + pub title: String, + pub description: String, + pub labels: Vec, + pub author: String, +} + +impl PullRequest { + pub async fn fetch(contribution: Contribution) -> Result { let request = reqwest::Client::new() .request( reqwest::Method::GET, format!( "https://api.github.com/repos/iced-rs/iced/pulls/{}", - self.id + contribution.id ), ) .header("User-Agent", "iced changelog generator") @@ -319,8 +345,8 @@ impl Candidate { let schema: Schema = request.send().await?.json().await?; - Ok(PullRequest { - id: self.id, + Ok(Self { + id: contribution.id, title: schema.title, description: schema.body, labels: schema.labels.into_iter().map(|label| label.name).collect(), @@ -339,6 +365,9 @@ pub enum Error { #[error("no GITHUB_TOKEN variable was set")] GitHubTokenNotFound, + + #[error("the changelog format is not valid")] + InvalidFormat, } impl From for Error { diff --git a/examples/changelog/src/main.rs b/examples/changelog/src/main.rs index 73da0f2c..79fa37b5 100644 --- a/examples/changelog/src/main.rs +++ b/examples/changelog/src/main.rs @@ -1,36 +1,33 @@ mod changelog; -mod icon; use crate::changelog::Changelog; -use iced::clipboard; use iced::font; use iced::widget::{ button, center, column, container, markdown, pick_list, progress_bar, rich_text, row, scrollable, span, stack, text, text_input, }; -use iced::{Element, Fill, FillPortion, Font, Task, Theme}; +use iced::{Center, Element, Fill, FillPortion, Font, Task, Theme}; pub fn main() -> iced::Result { iced::application("Changelog Generator", Generator::update, Generator::view) - .font(icon::FONT_BYTES) .theme(Generator::theme) .run_with(Generator::new) } enum Generator { Loading, - Empty, Reviewing { changelog: Changelog, - pending: Vec, + pending: Vec, state: State, preview: Vec, }, + Done, } enum State { - Loading(changelog::Candidate), + Loading(changelog::Contribution), Loaded { pull_request: changelog::PullRequest, description: Vec, @@ -42,7 +39,7 @@ enum State { #[derive(Debug, Clone)] enum Message { ChangelogListed( - Result<(Changelog, Vec), changelog::Error>, + Result<(Changelog, Vec), changelog::Error>, ), PullRequestFetched(Result), UrlClicked(markdown::Url), @@ -50,7 +47,8 @@ enum Message { CategorySelected(changelog::Category), Next, OpenPullRequest(u64), - CopyPreview, + ChangelogSaved(Result<(), changelog::Error>), + Quit, } impl Generator { @@ -64,23 +62,23 @@ impl Generator { fn update(&mut self, message: Message) -> Task { match message { Message::ChangelogListed(Ok((changelog, mut pending))) => { - if let Some(candidate) = pending.pop() { + if let Some(contribution) = pending.pop() { let preview = markdown::parse(&changelog.to_string()).collect(); *self = Self::Reviewing { changelog, pending, - state: State::Loading(candidate.clone()), + state: State::Loading(contribution.clone()), preview, }; Task::perform( - candidate.fetch(), + changelog::PullRequest::fetch(contribution), Message::PullRequestFetched, ) } else { - *self = Self::Empty; + *self = Self::Done; Task::none() } @@ -108,12 +106,6 @@ impl Generator { Task::none() } - Message::ChangelogListed(Err(error)) - | Message::PullRequestFetched(Err(error)) => { - log::error!("{error}"); - - Task::none() - } Message::UrlClicked(url) => { let _ = webbrowser::open(url.as_str()); @@ -172,19 +164,27 @@ impl Generator { { changelog.push(entry); + let save = Task::perform( + changelog.clone().save(), + Message::ChangelogSaved, + ); + *preview = markdown::parse(&changelog.to_string()).collect(); - if let Some(candidate) = pending.pop() { - *state = State::Loading(candidate.clone()); + if let Some(contribution) = pending.pop() { + *state = State::Loading(contribution.clone()); - Task::perform( - candidate.fetch(), - Message::PullRequestFetched, - ) + Task::batch([ + save, + Task::perform( + changelog::PullRequest::fetch(contribution), + Message::PullRequestFetched, + ), + ]) } else { - // TODO: We are done! - Task::none() + *self = Self::Done; + save } } else { Task::none() @@ -197,20 +197,32 @@ impl Generator { Task::none() } - Message::CopyPreview => { - let Self::Reviewing { changelog, .. } = self else { - return Task::none(); - }; + Message::ChangelogSaved(Ok(())) => Task::none(), - clipboard::write(changelog.to_string()) + Message::ChangelogListed(Err(error)) + | Message::PullRequestFetched(Err(error)) + | Message::ChangelogSaved(Err(error)) => { + log::error!("{error}"); + + Task::none() } + Message::Quit => iced::exit(), } } fn view(&self) -> Element { match self { Self::Loading => center("Loading...").into(), - Self::Empty => center("No changes found!").into(), + Self::Done => center( + column![ + text("Changelog is up-to-date! 🎉") + .shaping(text::Shaping::Advanced), + button("Quit").on_press(Message::Quit), + ] + .spacing(10) + .align_x(Center), + ) + .into(), Self::Reviewing { changelog, pending, @@ -237,8 +249,8 @@ impl Generator { }; let form: Element<_> = match state { - State::Loading(candidate) => { - text!("Loading #{}...", candidate.id).into() + State::Loading(contribution) => { + text!("Loading #{}...", contribution.id).into() } State::Loaded { pull_request, @@ -318,7 +330,7 @@ impl Generator { } }; - let preview: Element<_> = if preview.is_empty() { + let preview = if preview.is_empty() { center( container( text("The changelog is empty... so far!").size(12), @@ -326,9 +338,8 @@ impl Generator { .padding(10) .style(container::rounded_box), ) - .into() } else { - let content = container( + container( scrollable( markdown::view( preview, @@ -343,14 +354,7 @@ impl Generator { ) .width(Fill) .padding(10) - .style(container::rounded_box); - - let copy = button(icon::copy().size(12)) - .on_press(Message::CopyPreview) - .style(button::text); - - center(stack![content, container(copy).align_right(Fill)]) - .into() + .style(container::rounded_box) }; let review = column![container(form).height(Fill), progress]