use git;
use std::str;
use commit::{Commit, CommitList, Line};
use std::collections::HashMap;
use chrono::MIN_DATE;
use chrono::prelude::*;
use input::{Configuration, Conventions};
#[derive(Debug, Default, Serialize, Eq, PartialEq)]
pub struct ChangeLog {
pub scopes: Vec<Scope>,
pub commits: Vec<Commit>,
pub remote_url: Option<String>,
pub range: String,
pub date: String,
}
#[derive(Debug, Default, Serialize, Eq, PartialEq)]
pub struct Scope {
pub title: String,
pub categories: Vec<Category>,
}
#[derive(Debug, Default, Serialize, Eq, PartialEq)]
pub struct Category {
pub title: String,
pub changes: Vec<String>,
}
impl ChangeLog {
pub fn new() -> Self {
Self::from_log(Vec::new(), &Configuration::new())
}
pub fn from_range(range: &str, config: &Configuration) -> Self {
Self::from_log(vec![range.to_string()], config)
}
pub fn from_log(mut args: Vec<String>, config: &Configuration) -> Self {
if args.is_empty() {
if let Ok(Some(tag)) = git::last_tag() {
args.push(format!("{}..HEAD", tag))
} else {
args.push(String::from("HEAD^..HEAD"))
}
}
let header = args.join(" ");
let range = CommitList::from(args);
info!("Using revision range '{}'", range);
let mut log = Self::from(range, config);
log.range = header;
log
}
pub fn from<T: Iterator<Item = Commit>>(commits: T, config: &Configuration) -> Self {
let mut raw = RawReport::new();
let mut changelog = ChangeLog::default();
for commit in commits {
if raw.add(&commit, &config.conventions) {
trace!("Interesting commit {}", commit);
changelog.commits.push(commit);
} else {
debug!("No interesting changes in commit {}", &commit);
}
}
for scope in config.conventions.scope_titles() {
if let Some(mut categorized) = raw.slots.remove(&scope) {
let title = scope.to_owned();
let mut categories = Vec::new();
for category in config.conventions.category_titles() {
let title = category.to_owned();
if let Some(mut changes) = categorized.remove(&category) {
categories.push(Category { title, changes });
}
}
changelog.scopes.push({ Scope { title, categories } })
}
}
let remote = match config.output.remote {
Some(ref r) => r,
None => "origin",
};
changelog.remote_url = git::get_remote_url(remote).unwrap_or(None);
changelog.date = raw.date.format("%Y-%m-%d").to_string();
changelog
}
}
struct RawReport<'a> {
date: Date<Utc>,
slots: HashMap<&'a str, HashMap<&'a str, Vec<String>>>,
}
impl<'a> RawReport<'a> {
fn new() -> Self {
Self {
date: MIN_DATE,
slots: HashMap::default(),
}
}
fn add<'c>(&mut self, commit: &'c Commit, conventions: &'a Conventions) -> bool {
let mut interesting = false;
let mut current = Line::default();
for line in commit {
if line.category.is_some() {
interesting |= self.record(current, conventions);
current = Line::default();
current.scope = line.scope;
current.category = line.category;
}
if current.text.is_none() {
current.text = line.text
} else if let Some(mut text) = current.text.as_mut() {
text.push('\n');
text.push_str(&line.text.unwrap_or_default());
}
}
interesting |= self.record(current, conventions);
if let Ok(time) = DateTime::parse_from_rfc2822(&commit.time) {
let date = time.with_timezone(&Utc).date();
if date > self.date {
self.date = date;
}
}
interesting
}
fn record(&mut self, current: Line, conventions: &'a Conventions) -> bool {
let scope = conventions.scope_title(current.scope);
let category = conventions.category_title(current.category);
let interesting = category.is_some() && scope.is_some() && current.text.is_some();
if interesting {
self.slots
.entry(scope.unwrap())
.or_insert_with(HashMap::new)
.entry(category.unwrap())
.or_insert_with(Vec::new)
.push(current.text.unwrap());
}
interesting
}
}