Save CHANGELOG.md after each review in changelog tool
This commit is contained in:
parent
547e509683
commit
1be278e60c
4 changed files with 99 additions and 67 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue