mirror of
https://github.com/rust-lang/rust-analyzer
synced 2025-01-21 01:24:13 +00:00
169 lines
4.8 KiB
Rust
169 lines
4.8 KiB
Rust
use std::fmt::Write;
|
|
use std::{env, iter};
|
|
|
|
use anyhow::bail;
|
|
use xshell::{cmd, Shell};
|
|
|
|
pub(crate) fn get_changelog(
|
|
sh: &Shell,
|
|
changelog_n: usize,
|
|
commit: &str,
|
|
prev_tag: &str,
|
|
today: &str,
|
|
) -> anyhow::Result<String> {
|
|
let token = match env::var("GITHUB_TOKEN") {
|
|
Ok(token) => token,
|
|
Err(_) => bail!("Please obtain a personal access token from https://github.com/settings/tokens and set the `GITHUB_TOKEN` environment variable."),
|
|
};
|
|
|
|
let git_log = cmd!(sh, "git log {prev_tag}..HEAD --reverse").read()?;
|
|
let mut features = String::new();
|
|
let mut fixes = String::new();
|
|
let mut internal = String::new();
|
|
let mut others = String::new();
|
|
for line in git_log.lines() {
|
|
let line = line.trim_start();
|
|
if let Some(pr_num) = parse_pr_number(line) {
|
|
let accept = "Accept: application/vnd.github.v3+json";
|
|
let authorization = format!("Authorization: token {token}");
|
|
let pr_url = "https://api.github.com/repos/rust-lang/rust-analyzer/issues";
|
|
|
|
// we don't use an HTTPS client or JSON parser to keep the build times low
|
|
let pr = pr_num.to_string();
|
|
let pr_json =
|
|
cmd!(sh, "curl -s -H {accept} -H {authorization} {pr_url}/{pr}").read()?;
|
|
let pr_title = cmd!(sh, "jq .title").stdin(&pr_json).read()?;
|
|
let pr_title = unescape(&pr_title[1..pr_title.len() - 1]);
|
|
let pr_comment = cmd!(sh, "jq .body").stdin(pr_json).read()?;
|
|
|
|
let comments_json =
|
|
cmd!(sh, "curl -s -H {accept} -H {authorization} {pr_url}/{pr}/comments").read()?;
|
|
let pr_comments = cmd!(sh, "jq .[].body").stdin(comments_json).read()?;
|
|
|
|
let l = iter::once(pr_comment.as_str())
|
|
.chain(pr_comments.lines())
|
|
.rev()
|
|
.find_map(|it| {
|
|
let it = unescape(&it[1..it.len() - 1]);
|
|
it.lines().find_map(parse_changelog_line)
|
|
})
|
|
.into_iter()
|
|
.next()
|
|
.unwrap_or_else(|| parse_title_line(&pr_title));
|
|
let s = match l.kind {
|
|
PrKind::Feature => &mut features,
|
|
PrKind::Fix => &mut fixes,
|
|
PrKind::Internal => &mut internal,
|
|
PrKind::Other => &mut others,
|
|
PrKind::Skip => continue,
|
|
};
|
|
writeln!(s, "* pr:{pr_num}[] {}", l.message.as_deref().unwrap_or(&pr_title)).unwrap();
|
|
}
|
|
}
|
|
|
|
let contents = format!(
|
|
"\
|
|
= Changelog #{changelog_n}
|
|
:sectanchors:
|
|
:experimental:
|
|
:page-layout: post
|
|
|
|
Commit: commit:{commit}[] +
|
|
Release: release:{today}[]
|
|
|
|
== New Features
|
|
|
|
{features}
|
|
|
|
== Fixes
|
|
|
|
{fixes}
|
|
|
|
== Internal Improvements
|
|
|
|
{internal}
|
|
|
|
== Others
|
|
|
|
{others}
|
|
"
|
|
);
|
|
Ok(contents)
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum PrKind {
|
|
Feature,
|
|
Fix,
|
|
Internal,
|
|
Other,
|
|
Skip,
|
|
}
|
|
|
|
struct PrInfo {
|
|
message: Option<String>,
|
|
kind: PrKind,
|
|
}
|
|
|
|
fn unescape(s: &str) -> String {
|
|
s.replace(r#"\""#, "").replace(r#"\n"#, "\n").replace(r#"\r"#, "")
|
|
}
|
|
|
|
fn parse_pr_number(s: &str) -> Option<u32> {
|
|
const BORS_PREFIX: &str = "Merge #";
|
|
const HOMU_PREFIX: &str = "Auto merge of #";
|
|
if let Some(s) = s.strip_prefix(BORS_PREFIX) {
|
|
s.parse().ok()
|
|
} else if let Some(s) = s.strip_prefix(HOMU_PREFIX) {
|
|
if let Some(space) = s.find(' ') {
|
|
s[..space].parse().ok()
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn parse_changelog_line(s: &str) -> Option<PrInfo> {
|
|
let parts = s.splitn(3, ' ').collect::<Vec<_>>();
|
|
if parts.len() < 2 || parts[0] != "changelog" {
|
|
return None;
|
|
}
|
|
let message = parts.get(2).map(|it| it.to_string());
|
|
let kind = match parts[1].trim_end_matches(':') {
|
|
"feature" => PrKind::Feature,
|
|
"fix" => PrKind::Fix,
|
|
"internal" => PrKind::Internal,
|
|
"skip" => PrKind::Skip,
|
|
_ => {
|
|
let kind = PrKind::Other;
|
|
let message = format!("{} {}", parts[1], message.unwrap_or_default());
|
|
return Some(PrInfo { kind, message: Some(message) });
|
|
}
|
|
};
|
|
let res = PrInfo { message, kind };
|
|
Some(res)
|
|
}
|
|
|
|
fn parse_title_line(s: &str) -> PrInfo {
|
|
let lower = s.to_ascii_lowercase();
|
|
const PREFIXES: [(&str, PrKind); 5] = [
|
|
("feat: ", PrKind::Feature),
|
|
("feature: ", PrKind::Feature),
|
|
("fix: ", PrKind::Fix),
|
|
("internal: ", PrKind::Internal),
|
|
("minor: ", PrKind::Skip),
|
|
];
|
|
|
|
for &(prefix, kind) in &PREFIXES {
|
|
if lower.starts_with(prefix) {
|
|
let message = match &kind {
|
|
PrKind::Skip => None,
|
|
_ => Some(s[prefix.len()..].to_string()),
|
|
};
|
|
return PrInfo { message, kind };
|
|
}
|
|
}
|
|
PrInfo { kind: PrKind::Other, message: Some(s.to_string()) }
|
|
}
|