Save CHANGELOG.md after each review in changelog tool

This commit is contained in:
Héctor Ramón Jiménez 2024-09-18 00:21:56 +02:00
parent 547e509683
commit 1be278e60c
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
4 changed files with 99 additions and 67 deletions

View file

@ -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) - Fix `block_on` in `iced_wgpu` hanging Wasm builds. [#2313](https://github.com/iced-rs/iced/pull/2313)
Many thanks to... Many thanks to...
- @hecrj
- @n1ght-hunter - @n1ght-hunter
## [0.12.1] - 2024-02-22 ## [0.12.1] - 2024-02-22

View file

@ -29,7 +29,7 @@ impl Changelog {
} }
} }
pub async fn list() -> Result<(Self, Vec<Candidate>), Error> { pub async fn list() -> Result<(Self, Vec<Contribution>), Error> {
let mut changelog = Self::new(); 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() { for reviewed_entry in changelog.entries() {
candidates.retain(|candidate| candidate.id != reviewed_entry); candidates.retain(|candidate| candidate.id != reviewed_entry);
@ -106,6 +106,30 @@ impl Changelog {
Ok((changelog, candidates)) 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 { pub fn len(&self) -> usize {
self.ids.len() self.ids.len()
} }
@ -132,7 +156,7 @@ impl Changelog {
target.push(item); 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.push(entry.author);
self.authors.sort_by_key(|author| author.to_lowercase()); self.authors.sort_by_key(|author| author.to_lowercase());
} }
@ -238,21 +262,12 @@ impl fmt::Display for Category {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Candidate { pub struct Contribution {
pub id: u64, pub id: u64,
} }
#[derive(Debug, Clone)] impl Contribution {
pub struct PullRequest { pub async fn list() -> Result<Vec<Contribution>, Error> {
pub id: u64,
pub title: String,
pub description: String,
pub labels: Vec<String>,
pub author: String,
}
impl Candidate {
pub async fn list() -> Result<Vec<Candidate>, Error> {
let output = process::Command::new("git") let output = process::Command::new("git")
.args([ .args([
"log", "log",
@ -273,20 +288,31 @@ impl Candidate {
let (_, pull_request) = title.split_once("#")?; let (_, pull_request) = title.split_once("#")?;
let (pull_request, _) = pull_request.split_once([')', ' '])?; let (pull_request, _) = pull_request.split_once([')', ' '])?;
Some(Candidate { Some(Contribution {
id: pull_request.parse().ok()?, id: pull_request.parse().ok()?,
}) })
}) })
.collect()) .collect())
} }
}
pub async fn fetch(self) -> Result<PullRequest, Error> { #[derive(Debug, Clone)]
pub struct PullRequest {
pub id: u64,
pub title: String,
pub description: String,
pub labels: Vec<String>,
pub author: String,
}
impl PullRequest {
pub async fn fetch(contribution: Contribution) -> Result<Self, Error> {
let request = reqwest::Client::new() let request = reqwest::Client::new()
.request( .request(
reqwest::Method::GET, reqwest::Method::GET,
format!( format!(
"https://api.github.com/repos/iced-rs/iced/pulls/{}", "https://api.github.com/repos/iced-rs/iced/pulls/{}",
self.id contribution.id
), ),
) )
.header("User-Agent", "iced changelog generator") .header("User-Agent", "iced changelog generator")
@ -319,8 +345,8 @@ impl Candidate {
let schema: Schema = request.send().await?.json().await?; let schema: Schema = request.send().await?.json().await?;
Ok(PullRequest { Ok(Self {
id: self.id, id: contribution.id,
title: schema.title, title: schema.title,
description: schema.body, description: schema.body,
labels: schema.labels.into_iter().map(|label| label.name).collect(), labels: schema.labels.into_iter().map(|label| label.name).collect(),
@ -339,6 +365,9 @@ pub enum Error {
#[error("no GITHUB_TOKEN variable was set")] #[error("no GITHUB_TOKEN variable was set")]
GitHubTokenNotFound, GitHubTokenNotFound,
#[error("the changelog format is not valid")]
InvalidFormat,
} }
impl From<io::Error> for Error { impl From<io::Error> for Error {

View file

@ -1,36 +1,33 @@
mod changelog; mod changelog;
mod icon;
use crate::changelog::Changelog; use crate::changelog::Changelog;
use iced::clipboard;
use iced::font; use iced::font;
use iced::widget::{ use iced::widget::{
button, center, column, container, markdown, pick_list, progress_bar, button, center, column, container, markdown, pick_list, progress_bar,
rich_text, row, scrollable, span, stack, text, text_input, 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 { pub fn main() -> iced::Result {
iced::application("Changelog Generator", Generator::update, Generator::view) iced::application("Changelog Generator", Generator::update, Generator::view)
.font(icon::FONT_BYTES)
.theme(Generator::theme) .theme(Generator::theme)
.run_with(Generator::new) .run_with(Generator::new)
} }
enum Generator { enum Generator {
Loading, Loading,
Empty,
Reviewing { Reviewing {
changelog: Changelog, changelog: Changelog,
pending: Vec<changelog::Candidate>, pending: Vec<changelog::Contribution>,
state: State, state: State,
preview: Vec<markdown::Item>, preview: Vec<markdown::Item>,
}, },
Done,
} }
enum State { enum State {
Loading(changelog::Candidate), Loading(changelog::Contribution),
Loaded { Loaded {
pull_request: changelog::PullRequest, pull_request: changelog::PullRequest,
description: Vec<markdown::Item>, description: Vec<markdown::Item>,
@ -42,7 +39,7 @@ enum State {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Message { enum Message {
ChangelogListed( ChangelogListed(
Result<(Changelog, Vec<changelog::Candidate>), changelog::Error>, Result<(Changelog, Vec<changelog::Contribution>), changelog::Error>,
), ),
PullRequestFetched(Result<changelog::PullRequest, changelog::Error>), PullRequestFetched(Result<changelog::PullRequest, changelog::Error>),
UrlClicked(markdown::Url), UrlClicked(markdown::Url),
@ -50,7 +47,8 @@ enum Message {
CategorySelected(changelog::Category), CategorySelected(changelog::Category),
Next, Next,
OpenPullRequest(u64), OpenPullRequest(u64),
CopyPreview, ChangelogSaved(Result<(), changelog::Error>),
Quit,
} }
impl Generator { impl Generator {
@ -64,23 +62,23 @@ impl Generator {
fn update(&mut self, message: Message) -> Task<Message> { fn update(&mut self, message: Message) -> Task<Message> {
match message { match message {
Message::ChangelogListed(Ok((changelog, mut pending))) => { Message::ChangelogListed(Ok((changelog, mut pending))) => {
if let Some(candidate) = pending.pop() { if let Some(contribution) = pending.pop() {
let preview = let preview =
markdown::parse(&changelog.to_string()).collect(); markdown::parse(&changelog.to_string()).collect();
*self = Self::Reviewing { *self = Self::Reviewing {
changelog, changelog,
pending, pending,
state: State::Loading(candidate.clone()), state: State::Loading(contribution.clone()),
preview, preview,
}; };
Task::perform( Task::perform(
candidate.fetch(), changelog::PullRequest::fetch(contribution),
Message::PullRequestFetched, Message::PullRequestFetched,
) )
} else { } else {
*self = Self::Empty; *self = Self::Done;
Task::none() Task::none()
} }
@ -108,12 +106,6 @@ impl Generator {
Task::none() Task::none()
} }
Message::ChangelogListed(Err(error))
| Message::PullRequestFetched(Err(error)) => {
log::error!("{error}");
Task::none()
}
Message::UrlClicked(url) => { Message::UrlClicked(url) => {
let _ = webbrowser::open(url.as_str()); let _ = webbrowser::open(url.as_str());
@ -172,19 +164,27 @@ impl Generator {
{ {
changelog.push(entry); changelog.push(entry);
let save = Task::perform(
changelog.clone().save(),
Message::ChangelogSaved,
);
*preview = *preview =
markdown::parse(&changelog.to_string()).collect(); markdown::parse(&changelog.to_string()).collect();
if let Some(candidate) = pending.pop() { if let Some(contribution) = pending.pop() {
*state = State::Loading(candidate.clone()); *state = State::Loading(contribution.clone());
Task::perform( Task::batch([
candidate.fetch(), save,
Message::PullRequestFetched, Task::perform(
) changelog::PullRequest::fetch(contribution),
Message::PullRequestFetched,
),
])
} else { } else {
// TODO: We are done! *self = Self::Done;
Task::none() save
} }
} else { } else {
Task::none() Task::none()
@ -197,20 +197,32 @@ impl Generator {
Task::none() Task::none()
} }
Message::CopyPreview => { Message::ChangelogSaved(Ok(())) => Task::none(),
let Self::Reviewing { changelog, .. } = self else {
return 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<Message> { fn view(&self) -> Element<Message> {
match self { match self {
Self::Loading => center("Loading...").into(), 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 { Self::Reviewing {
changelog, changelog,
pending, pending,
@ -237,8 +249,8 @@ impl Generator {
}; };
let form: Element<_> = match state { let form: Element<_> = match state {
State::Loading(candidate) => { State::Loading(contribution) => {
text!("Loading #{}...", candidate.id).into() text!("Loading #{}...", contribution.id).into()
} }
State::Loaded { State::Loaded {
pull_request, pull_request,
@ -318,7 +330,7 @@ impl Generator {
} }
}; };
let preview: Element<_> = if preview.is_empty() { let preview = if preview.is_empty() {
center( center(
container( container(
text("The changelog is empty... so far!").size(12), text("The changelog is empty... so far!").size(12),
@ -326,9 +338,8 @@ impl Generator {
.padding(10) .padding(10)
.style(container::rounded_box), .style(container::rounded_box),
) )
.into()
} else { } else {
let content = container( container(
scrollable( scrollable(
markdown::view( markdown::view(
preview, preview,
@ -343,14 +354,7 @@ impl Generator {
) )
.width(Fill) .width(Fill)
.padding(10) .padding(10)
.style(container::rounded_box); .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()
}; };
let review = column![container(form).height(Fill), progress] let review = column![container(form).height(Fill), progress]