use indexmap::IndexMap;
use crate::{changelog::ChangeTypeMap, config::Config, version::Version};
use chrono::{Local, NaiveDateTime};
use regex::Regex;
pub const VERSION_COMMIT_MESSAGE: &str = "chore: 📝 update changelog and bump version to ";
pub fn commit_changes(version: String) {
let output = std::process::Command::new("git")
.args(&["add", "."])
.output()
.expect("Failed to execute command");
let output = String::from_utf8_lossy(&output.stdout);
println!("{}", output);
let output = std::process::Command::new("git")
.args(&[
"commit",
"-m",
format!("{VERSION_COMMIT_MESSAGE}{version}").as_str(),
])
.output()
.expect("Failed to execute command");
let output = String::from_utf8_lossy(&output.stdout);
println!("{}", output);
}
#[derive(Debug, PartialEq, Clone, Eq, Hash, PartialOrd, Ord)]
pub enum ChangeType {
Feature,
Fix,
Version,
Unknown,
}
impl std::fmt::Display for ChangeType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ChangeType::Feature => write!(f, "Feature"),
ChangeType::Fix => write!(f, "Fix"),
ChangeType::Version => write!(f, "Version"),
ChangeType::Unknown => write!(f, "Unknown"),
}
}
}
#[derive(Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord)]
pub struct Change {
pub message: String,
pub commit_id: String,
pub link: Option<String>,
pub author: String,
pub change_type: ChangeType,
pub date: NaiveDateTime,
}
fn parse_change(change: &str, config: &Config) -> Change {
let commit_id_regex = Regex::new(r"COMMIT_ID:(.*?)AUTHOR:").unwrap();
let author_regex = Regex::new(r"AUTHOR:(.*?)MESSAGE:").unwrap();
let message_regex = Regex::new(r"MESSAGE:(.*?)DATE:").unwrap();
let date_regex = Regex::new(r"DATE:(.*?)--date=iso-strict").unwrap();
let commit_id = commit_id_regex
.captures(change)
.unwrap()
.get(1)
.unwrap()
.as_str()
.trim()
.to_string();
let author = author_regex
.captures(change)
.unwrap()
.get(1)
.unwrap()
.as_str()
.trim()
.to_string();
let message = message_regex
.captures(change)
.unwrap()
.get(1)
.unwrap()
.as_str()
.replace(":sparkles:", "✨")
.replace(":bug:", "🐛") .trim()
.to_string();
let link = match &config.project_repo {
Some(repo) => {
let repo = repo.trim_end_matches('/');
if repo.contains("github") {
Some(format!(
"{repo}/commit/{commit_id}",
repo = repo,
commit_id = commit_id
))
} else if repo.contains("stash/projects") {
Some(format!(
"{repo}/commits/{commit_id}",
repo = repo,
commit_id = commit_id
))
} else {
None
}
}
None => None,
};
let change_type = if message.contains("Pull request #") {
ChangeType::Unknown
} else if message.starts_with("feat") {
ChangeType::Feature
} else if message.starts_with("fix") {
ChangeType::Fix
} else if message.contains(VERSION_COMMIT_MESSAGE) {
ChangeType::Version
} else {
ChangeType::Unknown
};
let change_date = date_regex
.captures(change)
.unwrap()
.get(1)
.unwrap()
.as_str()
.trim()
.to_string();
Change {
message,
commit_id,
link,
author,
change_type,
date: NaiveDateTime::parse_from_str(&change_date, "%a %b %d %T %Y %z").unwrap(),
}
}
pub fn get_changes(config: &Config, version: &Version) -> IndexMap<String, ChangeTypeMap> {
let output = std::process::Command::new("git")
.args(&[
"log",
"--pretty=format:COMMIT_ID:%H AUTHOR:%an MESSAGE:%s DATE:%cd --date=iso-strict",
])
.output()
.expect("Failed to execute command");
let output = String::from_utf8_lossy(&output.stdout);
let mut changes: Vec<Change> = output
.split("\n")
.collect::<Vec<&str>>()
.iter()
.filter(|change| change.len() > 0)
.map(|change| parse_change(change, config))
.filter(|change| change.change_type != ChangeType::Unknown)
.collect();
let mut version_changes = changes
.clone()
.into_iter()
.filter(|change| change.change_type == ChangeType::Version)
.collect::<Vec<Change>>();
version_changes.push(Change {
message: format!("{VERSION_COMMIT_MESSAGE}{version}"),
commit_id: "HEAD".to_string(),
change_type: ChangeType::Version,
author: "GitScribe".to_string(),
date: Local::now().naive_local(),
link: None,
});
version_changes.sort_by(|a, b| b.date.cmp(&a.date));
changes = changes
.into_iter()
.filter(|change| change.change_type != ChangeType::Version)
.collect();
let mut change_map: IndexMap<String, ChangeTypeMap> = IndexMap::new();
for version_change in &version_changes {
change_map.insert(
parse_change_for_version(&version_change.message),
ChangeTypeMap {
features: vec![],
fixes: vec![],
},
);
}
for change in &mut changes {
let release_change = version_changes
.iter()
.filter(|version_change| version_change.date >= change.date)
.min_by(|a, b| a.date.cmp(&b.date));
let version = parse_change_for_version(&release_change.unwrap().message);
match release_change {
Some(_) => {
if change_map.contains_key(&version) {
match change.change_type {
ChangeType::Feature => {
change_map
.get_mut(&version)
.unwrap()
.features
.push(change.clone());
}
ChangeType::Fix => {
change_map
.get_mut(&version)
.unwrap()
.fixes
.push(change.clone());
}
_ => {}
}
} else {
println!("Change Map Did not include the version.");
std::process::exit(1);
}
}
None => {}
}
}
change_map
}
fn parse_change_for_version(message: &String) -> String {
let version_regex = Regex::new(r"^.*(\d+\.\d+\.\d+)$").unwrap();
version_regex
.captures(message)
.unwrap()
.get(1)
.unwrap()
.as_str()
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use chrono::NaiveDateTime;
#[test]
fn test_parse_change() {
let config = Config::create_default();
let change = parse_change(
"COMMIT_ID:123abc AUTHOR:John Doe MESSAGE:feat: :sparkles: add new feature DATE:2024-02-10T00:40:40-05:00 --date=iso-strict",
&config,
);
assert_eq!(change.message, "✨ add new feature");
assert_eq!(change.commit_id, "123");
assert_eq!(change.author, "John Doe");
assert_eq!(change.link, None);
assert_eq!(change.change_type, ChangeType::Feature);
assert_eq!(
change.date,
NaiveDateTime::parse_from_str("2024-02-09", "%Y-%m-%d").unwrap()
);
}
#[test]
fn test_get_changes() {
let config = Config::create_default();
let changes = get_changes(&config, &Version::new("1.0.0".to_string()));
assert_eq!(changes.len() > 0, true);
}
}