Skip to content

Commit

Permalink
split in multiple files
Browse files Browse the repository at this point in the history
  • Loading branch information
IceSentry committed Feb 4, 2023
1 parent 02c2ef9 commit e79b365
Show file tree
Hide file tree
Showing 5 changed files with 322 additions and 309 deletions.
2 changes: 1 addition & 1 deletion generate-release/src/github_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ query {{
} else if login.is_string() {
vec![login.as_str().unwrap().to_string()]
} else {
bail!("Invalid login format, if it contains a null, it probably means we are being throttled.\n{json}");
bail!("Invalid login format. If it contains a null, it probably means we are being rate limited.\n{json}");
};
logins.extend(login);
}
Expand Down
61 changes: 61 additions & 0 deletions generate-release/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use crate::github_client::{GithubClient, GithubCommitResponse, GithubIssuesResponse};
use anyhow::Context;
use regex::Regex;

pub fn get_merged_prs(
client: &GithubClient,
since: &str,
sha: &str,
label: Option<&str>,
) -> anyhow::Result<Vec<(GithubIssuesResponse, GithubCommitResponse, String)>> {
println!("Getting list of all commits since: {since}");
// We use the list of commits to make sure the PRs are only on main
let commits = client
.get_commits(since, sha)
.context("Failed to get commits for branch")?;
println!("Found {} commits", commits.len());

println!("Getting list of all merged PRs since {since} with label {label:?}");
// We also get the list of merged PRs in batches instead of getting them separately for each commit
let prs = client.get_merged_prs(since, label)?;
println!("Found {} merged PRs", prs.len());

let mut out = vec![];
for commit in &commits {
let Some(title) = get_pr_title_from_commit(commit)else {
continue;
};

// Get the PR associated with the commit based on it's title
let Some(pr) = prs.iter().find(|pr| pr.title.contains(&title)) else {
// If there's no label, then not finding a PR is an issue because this means we want all PRs
// If there's a label then it just means the commit is not a PR with the label
if label.is_none() {
println!("\x1b[93mPR not found for {title} sha: {}\x1b[0m", commit.sha);
}
continue;
};
out.push((pr.clone(), commit.clone(), title));
}

Ok(out)
}

fn get_pr_title_from_commit(commit: &GithubCommitResponse) -> Option<String> {
let mut message_lines = commit.commit.message.lines();

// Title is always the first line of a commit message
let title = message_lines.next().expect("Commit message empty");

// Get the pr number added by bors at the end of the title
let re = Regex::new(r"\(#([\d]*)\)").unwrap();
let Some(cap) = re.captures_iter(title).last() else {
// This means there wasn't a PR associated with the commit
// Or bors didn't add a pr number
return None;
};
// remove PR number from title
let title = title.replace(&cap[0].to_string(), "");
let title = title.trim_end();
Some(title.to_string())
}
315 changes: 7 additions & 308 deletions generate-release/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
use anyhow::Context;
use clap::{Parser as ClapParser, Subcommand};
use github_client::{GithubClient, GithubCommitResponse, GithubIssuesResponse};
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
use regex::Regex;
use std::{
collections::{HashMap, HashSet},
fmt::Write,
path::PathBuf,
};
use migration_guide::generate_migration_guide;
use release_notes::generate_release_note;
use std::path::PathBuf;

mod github_client;
mod helpers;
mod migration_guide;
mod release_notes;

/// Generates markdown files used for a bevy releases.
///
Expand Down Expand Up @@ -96,302 +94,3 @@ fn main() -> anyhow::Result<()> {

Ok(())
}

/// Generates the list of contributors and a list of all closed PRs sorted by area labels
fn generate_release_note(
since: &str,
path: PathBuf,
client: &mut GithubClient,
) -> anyhow::Result<()> {
let main_sha = client
.get_branch_sha("main")
.context("Failed to get branch_sha")?;

println!("commit sha for main: {main_sha}");

let mut pr_map = HashMap::new();
let mut areas = HashMap::<String, Vec<i32>>::new();
let mut authors = HashSet::new();

let merged_prs = get_merged_prs(client, since, &main_sha, None)?;
for (pr, commit, title) in &merged_prs {
// Find authors and co-authors
// TODO this could probably be done with multiple threads to speed it up
'retry: {
match client.get_contributors(&commit.sha) {
Ok(logins) => {
if logins.is_empty() {
println!(
"\x1b[93mNo contributors found for https://github.com/bevyengine/bevy/pull/{} sha: {}\x1b[0m",
pr.number,
commit.sha
);
}
for login in logins {
authors.insert(login);
}
}
Err(err) => {
println!("\x1b[93m{err:?}\x1b[0m");
// 15 is mostly arbitrary, but it seems to work as intended
println!("Sleeping 15s to avoid being rate limited");
std::thread::sleep(std::time::Duration::from_secs(15));
break 'retry;
}
}
}

pr_map.insert(pr.number, title.to_string());

let area = if let Some(label) = pr.labels.iter().find(|l| l.name.starts_with("A-")) {
label.name.clone()
} else {
String::from("No area label")
};
areas.entry(area).or_default().push(pr.number);

authors.insert(pr.user.login.clone());
println!(
"[{title}](https://github.com/bevyengine/bevy/pull/{})",
pr.number
);
}

println!(
"Found {} prs merged by bors since {}",
merged_prs.len(),
since
);

let mut output = String::new();

writeln!(&mut output, "# Release Notes - {since}\n")?;

writeln!(&mut output, "## Contributors\n")?;
writeln!(&mut output, "A huge thanks to the {} contributors that made this release (and associated docs) possible! In random order:\n", authors.len())?;
for author in &authors {
writeln!(&mut output, "- @{author}")?;
}
writeln!(&mut output)?;

writeln!(&mut output, "## Full Changelog")?;

for (area, prs) in &areas {
writeln!(&mut output)?;
writeln!(&mut output, "## {area}")?;
writeln!(&mut output)?;

for pr_number in prs {
let Some(pr_title) = pr_map.get(pr_number) else {
continue;
};
writeln!(&mut output, "- [{pr_title}][{pr_number}]")?;
}
}

writeln!(&mut output)?;

for pr in pr_map.keys() {
writeln!(
&mut output,
"[{pr}]: https://github.com/bevyengine/bevy/pull/{pr}"
)?;
}

std::fs::write(path, output)?;

Ok(())
}

fn generate_migration_guide(
title: &str,
weight: i32,
date: &str,
path: PathBuf,
client: &mut GithubClient,
) -> anyhow::Result<()> {
let mut output = String::new();

// Write the frontmatter based on given parameters
write!(
&mut output,
r#"+++
title = "{title}"
weight = {weight}
sort_by = "weight"
template = "book-section.html"
page_template = "book-section.html"
insert_anchor_links = "right"
[extra]
long_title = "Migration Guide: {title}"
+++
Bevy relies heavily on improvements in the Rust language and compiler.
As a result, the Minimum Supported Rust Version (MSRV) is "the latest stable release" of Rust."#
)?;
writeln!(&mut output)?;

let main_sha = client
.get_branch_sha("main")
.context("Failed to get branch_sha")?;

println!("commit sha for main: {main_sha}");

let merged_breaking_prs = get_merged_prs(client, date, &main_sha, Some("C-Breaking-Change"))?;
for (pr, _, title) in &merged_breaking_prs {
println!("# {title}");

// Write title for the PR with correct heading and github url
writeln!(
&mut output,
"\n### [{}](https://github.com/bevyengine/bevy/pull/{})",
title, pr.number
)?;
write_markdown_section(pr.body.as_ref().unwrap(), "migration guide", &mut output)?;
}

println!(
"\nFound {} breaking PRs merged by bors",
merged_breaking_prs.len()
);

std::fs::write(path, output)?;

Ok(())
}

/// Writes the markdown section of the givent section header to the output.
/// The header name needs to be in lower case.
fn write_markdown_section(
body: &str,
section_header: &str,
output: &mut String,
) -> anyhow::Result<bool> {
// Parse the body of the PR
let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_SMART_PUNCTUATION);
let mut markdown = Parser::new_ext(body, options);
let mut section_found = false;

while let Some(event) = markdown.next() {
if let Event::Start(Tag::Heading(migration_guide_level, _, _)) = event {
// Find the section header
if let Some(Event::Text(heading_text)) = markdown.next() {
if !heading_text.to_lowercase().contains(section_header) {
continue;
}
}
section_found = true;
markdown.next(); // skip heading end

// Write the section's content
for event in markdown.by_ref() {
if let Event::Start(Tag::Heading(level, _, _)) = event {
if level >= migration_guide_level {
// go until next heading
break;
}
}
write_markdown_event(&event, output)?;
}
}
}

if !section_found {
// Someone didn't write a migration guide 😢
writeln!(output, "\n<!-- TODO -->")?;
println!("\x1b[93m{section_header} not found!\x1b[0m");
Ok(false)
} else {
Ok(true)
}
}

/// Write the markdown Event based on the Tag
/// This handles some edge cases like some code blocks not having a specified lang
/// This also makes sure the result has a more consistent formatting
fn write_markdown_event(event: &Event, output: &mut String) -> anyhow::Result<()> {
match event {
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) => writeln!(
output,
"\n```{}",
if lang.is_empty() {
"rust".to_string()
} else {
lang.to_string()
}
)?,
Event::End(Tag::CodeBlock(_)) => writeln!(output, "```")?,
Event::Start(Tag::Emphasis) | Event::End(Tag::Emphasis) => write!(output, "_")?,
// FIXME List currently always assume they are unordered
Event::Start(Tag::List(_)) => {}
Event::End(Tag::List(_)) => writeln!(output)?,
Event::Start(Tag::Item) => write!(output, "\n* ")?,
Event::End(Tag::Item) => {}
Event::Start(tag) | Event::End(tag) if matches!(tag, Tag::Paragraph) => writeln!(output)?,
Event::Text(text) => write!(output, "{text}")?,
Event::Code(text) => write!(output, "`{text}`")?,
Event::SoftBreak => writeln!(output)?,
_ => println!("unknown event {event:?}"),
};
Ok(())
}

fn get_merged_prs(
client: &GithubClient,
since: &str,
sha: &str,
label: Option<&str>,
) -> anyhow::Result<Vec<(GithubIssuesResponse, GithubCommitResponse, String)>> {
println!("Getting list of all commits since: {since}");
// We use the list of commits to make sure the PRs are only on main
let commits = client
.get_commits(since, sha)
.context("Failed to get commits for branch")?;
println!("Found {} commits", commits.len());

println!("Getting list of all merged PRs since {since} with label {label:?}");
// We also get the list of merged PRs in batches instead of getting them separately for each commit
let prs = client.get_merged_prs(since, label)?;
println!("Found {} merged PRs", prs.len());

let mut out = vec![];
for commit in &commits {
let Some(title) = get_pr_title_from_commit(commit)else {
continue;
};

// Get the PR associated with the commit based on it's title
let Some(pr) = prs.iter().find(|pr| pr.title.contains(&title)) else {
// If there's no label, then not finding a PR is an issue because this means we want all PRs
// If there's a label then it just means the commit is not a PR with the label
if label.is_none() {
println!("\x1b[93mPR not found for {title} sha: {}\x1b[0m", commit.sha);
}
continue;
};
out.push((pr.clone(), commit.clone(), title));
}

Ok(out)
}

fn get_pr_title_from_commit(commit: &GithubCommitResponse) -> Option<String> {
let mut message_lines = commit.commit.message.lines();

// Title is always the first line of a commit message
let title = message_lines.next().expect("Commit message empty");

// Get the pr number added by bors at the end of the title
let re = Regex::new(r"\(#([\d]*)\)").unwrap();
let Some(cap) = re.captures_iter(title).last() else {
// This means there wasn't a PR associated with the commit
// Or bors didn't add a pr number
return None;
};
// remove PR number from title
let title = title.replace(&cap[0].to_string(), "");
let title = title.trim_end();
Some(title.to_string())
}
Loading

0 comments on commit e79b365

Please sign in to comment.