use crate::git::gh_pr_title;
use crate::util::run_capture;
use regex::Regex;
use std::fmt::Write;
use std::path::PathBuf;
pub fn build_release_notes(
range: &str,
owner_repo: &str,
) -> anyhow::Result<String> {
let compare_url = {
let parts: Vec<_> = range.split("..").collect();
if parts.len() == 2 {
format!(
"https://github.com/{}/compare/{}...{}",
owner_repo, parts[0], parts[1]
)
} else {
format!(
"https://github.com/{}/compare/{}",
owner_repo, range
)
}
};
let raw = run_capture(
"git",
&["log", "--no-merges", "--pretty=- %h %s", range],
)
.unwrap_or_default();
let mut commits: Vec<String> = raw
.lines()
.filter(|l| !l.contains("docs(changelog):"))
.map(|s| s.to_string())
.collect();
if commits.is_empty() {
commits.push("(no changes)".to_string());
}
let re = Regex::new(r"\(#(\d+)\)").unwrap();
let mut pr_nums: Vec<String> = vec![];
for c in &commits {
for cap in re.captures_iter(c) {
pr_nums.push(cap[1].to_string());
}
}
pr_nums.sort();
pr_nums.dedup();
let mut prs_section: Vec<String> = vec![];
for n in &pr_nums {
if let Some(title) = gh_pr_title(owner_repo, n) {
prs_section.push(format!("- #{}: {}", n, title));
} else {
prs_section.push(format!("- #{}", n));
}
}
let authors_raw = run_capture("git", &["shortlog", "-sn", range])
.unwrap_or_else(|_| "(no new authors)".to_string());
let authors = if authors_raw.trim().is_empty() {
"(no new authors)".to_string()
} else {
let mut out = String::new();
for line in authors_raw.lines() {
let line = line.trim_start();
let mut sp = line.splitn(2, ' ');
let count = sp.next().unwrap_or("0");
let name = sp.next().unwrap_or("").trim();
let _ =
writeln!(&mut out, "- {} ({} commits)", name, count);
}
out
};
let mut s = String::new();
writeln!(
&mut s,
"### Changes since {}",
range.split("..").next().unwrap_or("")
)?;
for c in &commits {
writeln!(&mut s, "{}", c)?;
}
writeln!(&mut s)?;
if !prs_section.is_empty() {
writeln!(&mut s, "### Pull requests")?;
for p in &prs_section {
writeln!(&mut s, "{}", p)?;
}
writeln!(&mut s)?;
}
writeln!(&mut s, "### Authors")?;
write!(&mut s, "{}", authors)?;
writeln!(&mut s)?;
writeln!(&mut s, "### Compare")?;
writeln!(&mut s, "{}", compare_url)?;
Ok(s)
}
pub fn build_release_notes_scoped(
range: &str,
owner_repo: &str,
paths: &[PathBuf],
) -> anyhow::Result<String> {
let repo_root = crate::util::run_capture(
"git",
&["rev-parse", "--show-toplevel"],
)
.unwrap_or_else(|_| ".".to_string());
let repo_root_pb = std::path::PathBuf::from(repo_root);
let compare_url = {
let parts: Vec<_> = range.split("..").collect();
if parts.len() == 2 {
format!(
"https://github.com/{}/compare/{}...{}",
owner_repo, parts[0], parts[1]
)
} else {
format!(
"https://github.com/{}/compare/{}",
owner_repo, range
)
}
};
let mut args =
vec!["log", "--no-merges", "--pretty=- %h %s", range];
let mut shortlog_args = vec!["shortlog", "-sn", range];
if !paths.is_empty() {
args.push("--");
shortlog_args.push("--");
}
let mut args_vec: Vec<String> =
args.iter().map(|s| s.to_string()).collect();
let mut shortlog_vec: Vec<String> =
shortlog_args.iter().map(|s| s.to_string()).collect();
for p in paths {
let rel = if p.is_absolute() {
p.strip_prefix(&repo_root_pb).unwrap_or(p).to_path_buf()
} else {
p.to_path_buf()
};
let s = rel.to_string_lossy().to_string();
args_vec.push(s.clone());
shortlog_vec.push(s);
}
let raw = run_capture(
"git",
&args_vec.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
)
.unwrap_or_default();
let mut commits: Vec<String> = raw
.lines()
.filter(|l| !l.contains("docs(changelog):"))
.map(|s| s.to_string())
.collect();
if commits.is_empty() {
commits.push("(no changes)".to_string());
}
let re = Regex::new(r"\(#(\d+)\)").unwrap();
let mut pr_nums: Vec<String> = vec![];
for c in &commits {
for cap in re.captures_iter(c) {
pr_nums.push(cap[1].to_string());
}
}
pr_nums.sort();
pr_nums.dedup();
let mut prs_section: Vec<String> = vec![];
for n in &pr_nums {
if let Some(title) = gh_pr_title(owner_repo, n) {
prs_section.push(format!("- #{}: {}", n, title));
} else {
prs_section.push(format!("- #{}", n));
}
}
let authors_raw = run_capture(
"git",
&shortlog_vec.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
)
.unwrap_or_else(|_| "(no new authors)".to_string());
let authors = if authors_raw.trim().is_empty() {
"(no new authors)".to_string()
} else {
let mut out = String::new();
for line in authors_raw.lines() {
let line = line.trim_start();
let mut sp = line.splitn(2, ' ');
let count = sp.next().unwrap_or("0");
let name = sp.next().unwrap_or("").trim();
let _ =
writeln!(&mut out, "- {} ({} commits)", name, count);
}
out
};
let mut s = String::new();
writeln!(
&mut s,
"### Changes since {}",
range.split("..").next().unwrap_or("")
)?;
for c in &commits {
writeln!(&mut s, "{}", c)?;
}
writeln!(&mut s)?;
if !prs_section.is_empty() {
writeln!(&mut s, "### Pull requests")?;
for p in &prs_section {
writeln!(&mut s, "{}", p)?;
}
writeln!(&mut s)?;
}
writeln!(&mut s, "### Authors")?;
write!(&mut s, "{}", authors)?;
writeln!(&mut s)?;
writeln!(&mut s, "### Compare")?;
writeln!(&mut s, "{}", compare_url)?;
Ok(s)
}