use git;
use std::{fmt, str};
use nom::{is_alphanumeric, IResult};
#[derive(Debug, Default, Serialize, Eq, PartialEq)]
pub struct Commit {
pub sha: String,
pub author: String,
pub time: String,
pub summary: String,
pub number: Option<u32>,
pub message: String,
}
pub struct CommitList {
input: String,
commits: Vec<String>,
}
pub struct CommitMessage<'a>(Vec<&'a str>);
#[derive(Default, Debug)]
pub struct Line {
pub scope: Option<String>,
pub category: Option<String>,
pub text: Option<String>,
}
impl<T: AsRef<str>> From<T> for Commit {
fn from(input: T) -> Self {
let revision = input.as_ref();
match git::get_commit_message(revision) {
Ok(lines) => Commit::from_lines(lines),
Err(why) => {
error!("Commit {} will be skipped (Reason: {})", revision, why);
Commit::default()
}
}
}
}
impl Commit {
pub fn from_lines(mut lines: Vec<String>) -> Self {
let mut commit = Self::default();
commit.sha = lines.remove(0);
commit.author = lines.remove(0);
commit.time = lines.remove(0);
let subject = lines.remove(0);
commit.number = parse_number(&subject);
commit.summary = parse_subject(&subject);
commit.message = lines.join("\n");
commit
}
}
impl<'a> From<&'a str> for CommitList {
fn from(range: &str) -> Self {
Self::from(vec![range.to_string()])
}
}
impl From<Vec<String>> for CommitList {
fn from(git_log_args: Vec<String>) -> Self {
let input = git_log_args.join(" ");
let commits = match git::commits_in_log(&git_log_args) {
Ok(commits) => commits,
Err(why) => {
error!("Invalid log input {} (Reason: {})", input, why);
vec![]
}
};
CommitList { commits, input }
}
}
impl Iterator for CommitList {
type Item = Commit;
fn next(&mut self) -> Option<Self::Item> {
self.commits.pop().map(Commit::from)
}
}
impl<'a> IntoIterator for &'a Commit {
type Item = Line;
type IntoIter = CommitMessage<'a>;
fn into_iter(self) -> Self::IntoIter {
CommitMessage(self.message.lines().collect())
}
}
impl<'a> Iterator for CommitMessage<'a> {
type Item = Line;
fn next(&mut self) -> Option<Self::Item> {
if self.0.is_empty() {
None
} else {
Some(parse_line(self.0.remove(0)))
}
}
}
impl fmt::Display for Commit {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}:{}", self.sha, self.summary)
}
}
impl fmt::Display for CommitList {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} ({} commits)", self.input, self.commits.len())
}
}
fn parse_subject(line: &str) -> String {
let first_open = line.find("(#").unwrap_or_else(|| line.len());
String::from(line.get(0..first_open).unwrap_or_else(|| line).trim())
}
fn parse_number(line: &str) -> Option<u32> {
let last_open = line.rfind("(#");
let last_close = line.rfind(')');
if last_open.is_none() || last_close.is_none() {
None
} else {
let end = last_close.unwrap();
let start = last_open.unwrap() + "(#".len();
let num = line.get(start..end).map(|s| s.parse().ok());
num.unwrap_or(None)
}
}
fn parse_line(line: &str) -> Line {
match tagged_change(line) {
IResult::Done(_, l) => l,
_ => Line::default(),
}
}
named!(tagged_change<&str, Line>,
alt!(with_category
| with_category_scope
| with_category_text
| with_category_scope_text
| with_text
));
named!(with_text<&str, Line>,
do_parse!(opt!(tag!("-")) >>
text: whatever >>
(Line{
scope: None,
category: None,
text: Some(text)
})));
named!(with_category<&str, Line>,
do_parse!(
tag!("-") >> category: tagname >>
tag!(":") >> eof!() >>
(Line{
scope: None,
category: Some(category),
text: None
})));
named!(with_category_scope<&str, Line>,
do_parse!(
tag!("-") >> category: tagname >>
tag!("(") >> scope: tagname >>
tag!("):") >> eof!() >>
(Line{
scope: Some(scope),
category: Some(category),
text: None
})));
named!(with_category_text<&str, Line>,
do_parse!(
tag!("-") >> category: tagname >>
tag!(":") >> text: whatever >>
(Line{
scope: None,
category: Some(category),
text: Some(text)
})));
named!(with_category_scope_text<&str, Line>,
do_parse!(
tag!("-") >> category: tagname >>
tag!("(") >> scope: tagname >>
tag!("):") >> text: whatever >>
(Line{
scope: Some(scope),
category: Some(category),
text: Some(text)
})));
named!(whatever<&str, String>,
map!(take_while1_s!(|_| true), String::from));
named!(tagname<&str, String>,
map!(ws!(take_while1_s!(|c| is_alphanumeric(c as u8))), str::to_lowercase));
#[cfg(test)]
mod tests {
#[test]
fn commit_fetch() {
use super::{Commit, CommitList};
let head = Commit::from("2c5dda2e");
let list = CommitList::from("2c5dda2e^..2c5dda2e");
assert_eq!(list.to_string(), "2c5dda2e^..2c5dda2e (1 commits)");
let also_head = list.into_iter().next().unwrap();
assert_eq!(head.sha, also_head.sha);
assert!(head.to_string().starts_with("2c5dda2e"));
}
#[test]
fn negative() {
assert!(super::Commit::from("no-such-commit").summary.is_empty());
assert_eq!(super::CommitList::from("bad-range").into_iter().count(), 0);
}
#[test]
fn commit_lines() {
use super::Commit;
let reference = &Commit::from("2c5dda2e5ec6d0ad7abdcd20661bf2cb846ee5f2");
assert_eq!(reference.into_iter().count(), 17);
}
#[test]
fn commit_parse_summary() {
use super::{parse_number, parse_subject};
let message = "foo bar (#123)";
assert_eq!(parse_subject(message), "foo bar");
assert_eq!(parse_number(message), Some(123));
let message = "foo bar ()()";
assert_eq!(parse_subject(message), message);
assert_eq!(parse_number(message), None);
let message = "foo bar #123 (#101)(#103)";
assert_eq!(parse_subject(message), "foo bar #123");
assert_eq!(parse_number(message), Some(103));
}
#[test]
fn commit_parse_line() {
use commit::parse_line;
let line = parse_line("- break(shell): foo bar");
assert_eq!(Some(String::from("shell")), line.scope);
assert_eq!(Some(String::from("break")), line.category);
assert_eq!(Some(String::from(" foo bar")), line.text);
let line = parse_line("-BREAK ( Shell ): foo bar");
assert_eq!(Some(String::from("shell")), line.scope);
assert_eq!(Some(String::from("break")), line.category);
assert_eq!(Some(String::from(" foo bar")), line.text);
let line = parse_line("- break(shell):");
assert_eq!(Some(String::from("shell")), line.scope);
assert_eq!(Some(String::from("break")), line.category);
assert_eq!(None, line.text);
let line = parse_line("- break ( SHELL ):");
assert_eq!(Some(String::from("shell")), line.scope);
assert_eq!(Some(String::from("break")), line.category);
assert_eq!(None, line.text);
let line = parse_line("-fix:");
assert_eq!(None, line.scope);
assert_eq!(Some(String::from("fix")), line.category);
assert_eq!(None, line.text);
let line = parse_line("- fix: foo bar");
assert_eq!(None, line.scope);
assert_eq!(Some(String::from("fix")), line.category);
assert_eq!(Some(String::from(" foo bar")), line.text);
let line = parse_line("- FIX : foo bar");
assert_eq!(None, line.scope);
assert_eq!(Some(String::from("fix")), line.category);
assert_eq!(Some(String::from(" foo bar")), line.text);
let line = parse_line("- foo bar");
assert_eq!(None, line.scope);
assert_eq!(None, line.category);
assert_eq!(Some(String::from(" foo bar")), line.text);
let line = parse_line("foo bar");
assert_eq!(None, line.scope);
assert_eq!(None, line.category);
assert_eq!(Some(String::from("foo bar")), line.text);
let line = parse_line("");
assert_eq!(None, line.text);
assert_eq!(None, line.scope);
assert_eq!(None, line.category);
}
}