use crate::errors::*;
use crate::utils;
use crate::utils::SEPARATOR_LINE;
use regex::Regex;
use std::collections::{BTreeMap, HashMap};
use std::fmt::Write;
use std::sync::LazyLock;
const PULL_REQUEST_NUMBER_PATTERN: &str = r#"#\d+"#;
pub static RE_PULL_REQUEST_NUMBER: LazyLock<Regex> = LazyLock::new(|| Regex::new(PULL_REQUEST_NUMBER_PATTERN).unwrap());
#[derive(Clone)]
struct Commit {
hash: String,
subject: String,
}
#[derive(Clone)]
struct Issue {
number: String,
title: String,
url: String,
}
#[derive(Clone)]
struct PullRequest {
number: String,
title: String,
url: String,
commits: Vec<Commit>,
}
#[allow(clippy::too_many_arguments)]
pub fn get_changelog(
verbose: bool,
start_revision: &str,
end_revision: &str,
milestone: &str,
repository: &str,
dir: &str,
exclude_commit: Vec<String>,
exclude_pr: Vec<String>,
) -> Result<String> {
if verbose {
println!("\nCOMMANDS");
println!("{SEPARATOR_LINE}");
}
let issues = get_issues(verbose, milestone, repository)?;
let pull_requests = get_pull_requests(verbose, milestone, repository)?;
let commits = get_commits(verbose, dir, start_revision, end_revision)?;
if verbose {
println!("\nISSUES");
println!("{SEPARATOR_LINE}");
for issue in &issues {
println!("{} | {} | {}", issue.number, issue.title, issue.url);
}
println!("\nPULL REQUESTS");
println!("{SEPARATOR_LINE}");
for pull_request in &pull_requests {
println!("{} | {} | {}", pull_request.number, pull_request.title, pull_request.url);
for commit in &pull_request.commits {
println!(" {} | {}", commit.hash, commit.subject);
}
}
println!("\nCOMMITS");
println!("{SEPARATOR_LINE}");
for commit in &commits {
println!("{} | {}", commit.hash, commit.subject);
}
}
let mut commit_map = HashMap::new();
for commit in &commits {
commit_map.insert(commit.hash.clone(), commit.clone());
}
if verbose {
println!("\nEXCLUDED COMMITS:");
println!("{SEPARATOR_LINE}");
}
for commit in &commits {
for pattern in &exclude_commit {
if commit.subject.contains(pattern) {
if verbose {
println!("{} | {} | {}", commit.hash, commit.subject, pattern);
}
commit_map.remove(&commit.hash);
}
}
}
let mut issue_sorted_map = BTreeMap::new();
for issue in &issues {
issue_sorted_map.insert(issue.number.clone(), issue.clone());
}
let mut pull_request_map = BTreeMap::new();
for pull_request in &pull_requests {
for commit in &pull_request.commits {
commit_map.remove(&commit.hash);
}
pull_request_map.insert(pull_request.number.clone(), pull_request.clone());
}
let mut warnings = BTreeMap::new();
for commit in &commits {
if let Some(captures) = RE_PULL_REQUEST_NUMBER.captures(&commit.subject) {
let number = captures[0][1..].to_string();
if pull_request_map.contains_key(&number) {
commit_map.remove(&commit.hash);
} else {
warnings.insert(number.clone(), format!("PR: #{} not in milestone {} | {}", number, milestone, commit.subject));
}
}
}
if verbose {
println!("\nEXCLUDED PULL REQUESTS");
println!("{SEPARATOR_LINE}");
}
for pull_request in &pull_requests {
for pattern in &exclude_pr {
if pull_request.title.contains(pattern) {
if verbose {
println!("{} | {} | {} | {}", pull_request.number, pull_request.title, pull_request.url, pattern);
}
pull_request_map.remove(&pull_request.number);
}
}
}
let mut changelog = String::new();
for issue in issue_sorted_map.values().rev() {
let _ = writeln!(&mut changelog, "- {} ([#{}])", issue.title, issue.number);
}
for pull_request in pull_request_map.values().rev() {
let _ = writeln!(&mut changelog, "- {} ([#{}])", pull_request.title, pull_request.number);
}
for commit in commit_map.values() {
let _ = writeln!(&mut changelog, "- {} ([0x{}])", commit.subject, &commit.hash[..7]);
}
let _ = writeln!(&mut changelog);
for issue in issue_sorted_map.values().rev() {
let _ = writeln!(&mut changelog, "[#{}]: {}", issue.number, issue.url);
}
for pull_request in pull_request_map.values().rev() {
let _ = writeln!(&mut changelog, "[#{}]: {}", pull_request.number, pull_request.url);
}
for commit in commit_map.values() {
let _ = writeln!(&mut changelog, "[0x{}]: https://github.com/{repository}/commit/{}", &commit.hash[..7], commit.hash);
}
if !warnings.is_empty() {
let _ = writeln!(&mut changelog, "\nWARNINGS:");
for warning in warnings.values().rev() {
let _ = writeln!(&mut changelog, "{}", warning);
}
}
Ok(changelog)
}
fn parse_issues(input: String) -> Result<Vec<Issue>> {
let mut issues = vec![];
let rows = utils::parse_columns(input, 3)?;
for columns in rows {
issues.push(Issue {
number: columns[0].to_string(),
title: columns[1].to_string(),
url: columns[2].to_string(),
});
}
Ok(issues)
}
fn get_issues(verbose: bool, milestone: &str, repository: &str) -> Result<Vec<Issue>> {
let search = format!(r#"--search=milestone:{}"#, milestone);
let repo = format!("--repo={}", repository);
let args = &[
"issue",
"list",
search.as_str(),
"--state=all",
"--limit=9999",
repo.as_str(),
"--json=number,title,url",
r#"--template='{{range .}}{{printf "%v ||| %s ||| %s\n" .number .title .url}}{{end}}'"#,
];
let stdout = utils::execute_command(verbose, "gh", args, ".")?;
parse_issues(stdout)
}
fn get_pull_request_commits(verbose: bool, number: &str, repository: &str) -> Result<Vec<Commit>> {
let repo = format!("--repo={}", repository);
let args = &[
"pr",
"view",
number,
repo.as_str(),
"--json=commits",
r#"--jq=.commits[]|"\(.oid) ||| \(.messageHeadline)""#,
];
let stdout = utils::execute_command(verbose, "gh", args, ".")?;
parse_commits(stdout)
}
fn parse_pull_requests(verbose: bool, input: String, repository: &str) -> Result<Vec<PullRequest>> {
let mut pull_requests = vec![];
let rows = utils::parse_columns(input, 3)?;
for columns in rows {
let number = columns[0].to_string();
let commits = get_pull_request_commits(verbose, &number, repository)?;
pull_requests.push(PullRequest {
number,
title: columns[1].to_string(),
url: columns[2].to_string(),
commits,
});
}
Ok(pull_requests)
}
fn get_pull_requests(verbose: bool, milestone: &str, repository: &str) -> Result<Vec<PullRequest>> {
let search = format!(r#"--search=milestone:{}"#, milestone);
let repo = format!("--repo={}", repository);
let args = &[
"pr",
"list",
search.as_str(),
"--state=all",
"--limit=9999",
repo.as_str(),
"--json=number,title,url",
r#"--template='{{range .}}{{printf "%v ||| %s ||| %s\n" .number .title .url}}{{end}}'"#,
];
let stdout = utils::execute_command(verbose, "gh", args, ".")?;
parse_pull_requests(verbose, stdout, repository)
}
fn parse_commits(input: String) -> Result<Vec<Commit>> {
let mut commits = vec![];
let rows = utils::parse_columns(input, 2)?;
for columns in rows {
commits.push(Commit {
hash: columns[0].to_string(),
subject: columns[1].to_string(),
});
}
Ok(commits)
}
fn get_commits(verbose: bool, dir: &str, start_revision: &str, end_revision: &str) -> Result<Vec<Commit>> {
let revisions = format!("{}...{}", start_revision, end_revision);
let args = &["log", r#"--format="%H ||| %s""#, revisions.as_str(), "--"];
let stdout = utils::execute_command(verbose, "git", args, dir)?;
parse_commits(stdout)
}