diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 571ea27011..3a91481bab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -224,7 +224,8 @@ Instructions for mdBook maintainers to publish a new release: 1. Create a PR to update the version and update the CHANGELOG: 1. Update the version in `Cargo.toml` 2. Run `cargo xtask test-all` to verify that everything is passing, and to update `Cargo.lock`. - 3. Update `CHANGELOG.md` with any changes that users may be interested in. + 3. Run `cargo xtask changelog` to add a new entry to the changelog. + 1. This will add a list of all changes at the top. You will need to move those into the appropriate categories. Most changes that are generally not relevant to a user should be removed. Rewrite the descriptions so that a user can reasonably figure out what it means. 4. Commit the changes, and open a PR. 2. After the PR has been merged, create a release in GitHub. This can either be done in the GitHub web UI, or on the command-line: ```bash diff --git a/crates/xtask/src/changelog.rs b/crates/xtask/src/changelog.rs new file mode 100644 index 0000000000..99bf297f59 --- /dev/null +++ b/crates/xtask/src/changelog.rs @@ -0,0 +1,120 @@ +//! Helper to generate a changelog for a new release. + +use super::Result; +use std::fs; +use std::process::Command; +use std::process::exit; + +const CHANGELOG_PATH: &str = "CHANGELOG.md"; + +pub(crate) fn changelog() -> Result<()> { + let previous = get_previous()?; + let current = get_current()?; + if current == previous { + eprintln!( + "error: Current version is `{current}` which is the same as the \ + previous version in the changelog. Run `cargo set-version --bump first." + ); + exit(1); + } + let prs = get_prs(&previous)?; + update_changelog(&previous, ¤t, &prs)?; + Ok(()) +} + +fn get_previous() -> Result { + let contents = fs::read_to_string(CHANGELOG_PATH)?; + let version = contents + .lines() + .filter_map(|line| line.strip_prefix("## mdBook ")) + .next() + .expect("at least one entry") + .to_owned(); + Ok(version) +} + +fn get_current() -> Result { + let contents = fs::read_to_string("Cargo.toml")?; + let mut lines = contents + .lines() + .filter_map(|line| line.strip_prefix("version = ")) + .map(|version| &version[1..version.len() - 1]); + let version = lines.next().expect("version should exist").to_owned(); + assert_eq!(lines.next(), None); + Ok(version) +} + +fn get_prs(previous: &str) -> Result> { + println!("running `git fetch upstream`"); + let status = Command::new("git").args(["fetch", "upstream"]).status()?; + if !status.success() { + eprintln!("error: git fetch failed"); + exit(1); + } + println!("running `git log`"); + const SEPARATOR: &str = "---COMMIT_SEPARATOR---"; + let output = Command::new("git") + .args([ + "log", + "--first-parent", + &format!("--pretty=format:%B%n{SEPARATOR}"), + "upstream/master", + &format!("v{previous}...upstream/HEAD"), + ]) + .output()?; + if !output.status.success() { + eprintln!("error: git log failed"); + exit(1); + } + let stdout = std::str::from_utf8(&output.stdout).unwrap(); + let prs = stdout + .split(&format!("{SEPARATOR}\n")) + .filter_map(|entry| { + let mut lines = entry.lines(); + let first = match lines.next().unwrap().strip_prefix("Merge pull request #") { + Some(f) => f, + None => { + println!("warning: merge line not found in {entry}"); + return None; + } + }; + let number = first.split_whitespace().next().unwrap(); + assert_eq!(lines.next(), Some("")); + let title = lines.next().expect("title is set"); + assert_eq!(lines.next(), Some("")); + Some((number.to_string(), title.to_string())) + }) + .collect(); + Ok(prs) +} + +fn update_changelog(previous: &str, current: &str, prs: &[(String, String)]) -> Result<()> { + let prs: String = prs + .iter() + .map(|(number, title)| { + format!( + "- {title}\n \ + [#{number}](https://github.com/rust-lang/mdBook/pull/{number})\n" + ) + }) + .collect(); + let new = format!( + "## mdBook {current}\n\ + [v{previous}...v{current}](https://github.com/rust-lang/mdBook/compare/v{previous}...v{current})\n\ + \n\ + {prs}\ + \n\ + ### Added\n\ + \n\ + ### Changed\n\ + \n\ + ### Fixed\n\ + \n" + ); + + let mut contents = fs::read_to_string(CHANGELOG_PATH)?; + let insertion_point = contents.find("## ").unwrap(); + contents.insert_str(insertion_point, &new); + fs::write(CHANGELOG_PATH, contents)?; + Ok(()) +} diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index 6bd891cdb8..26aea306ba 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -5,11 +5,13 @@ use std::error::Error; use std::process::Command; use std::process::exit; +mod changelog; + type Result = std::result::Result>; fn main() -> Result<()> { macro_rules! commands { - ($($name:literal => $func:ident),* $(,)?) => { + ($($name:literal => $func:expr),* $(,)?) => { [$(($name, $func as fn() -> Result<()>)),*] }; } @@ -23,6 +25,7 @@ fn main() -> Result<()> { "semver-checks" => semver_checks, "eslint" => eslint, "gui" => gui, + "changelog" => changelog::changelog, } .into_iter() .collect();