1use git;
5use std::{fmt, str};
6use nom::{is_alphanumeric, IResult};
7
8#[derive(Debug, Default, Serialize, Eq, PartialEq)]
10pub struct Commit {
11 pub sha: String,
13
14 pub author: String,
16
17 pub time: String,
19
20 pub summary: String,
22
23 pub number: Option<u32>,
25
26 pub message: String,
28}
29
30pub struct CommitList {
32 input: String,
34
35 commits: Vec<String>,
37}
38
39pub struct CommitMessage<'a>(Vec<&'a str>);
41
42#[derive(Default, Debug)]
44pub struct Line {
45 pub scope: Option<String>,
47
48 pub category: Option<String>,
50
51 pub text: Option<String>,
53}
54
55impl<T: AsRef<str>> From<T> for Commit {
56 fn from(input: T) -> Self {
58 let revision = input.as_ref();
59 match git::get_commit_message(revision) {
60 Ok(lines) => Commit::from_lines(lines),
61 Err(why) => {
62 error!("Commit {} will be skipped (Reason: {})", revision, why);
63 Commit::default()
64 }
65 }
66 }
67}
68
69impl Commit {
70 pub fn from_lines(mut lines: Vec<String>) -> Self {
71 let mut commit = Self::default();
72
73 commit.sha = lines.remove(0);
74 commit.author = lines.remove(0);
75 commit.time = lines.remove(0);
76
77 let subject = lines.remove(0);
78 commit.number = parse_number(&subject);
79 commit.summary = parse_subject(&subject);
80 commit.message = lines.join("\n");
81
82 commit
83 }
84}
85
86impl<'a> From<&'a str> for CommitList {
87 fn from(range: &str) -> Self {
89 Self::from(vec![range.to_string()])
90 }
91}
92
93impl From<Vec<String>> for CommitList {
94 fn from(git_log_args: Vec<String>) -> Self {
96 let input = git_log_args.join(" ");
98
99 let commits = match git::commits_in_log(&git_log_args) {
101 Ok(commits) => commits,
102 Err(why) => {
103 error!("Invalid log input {} (Reason: {})", input, why);
104 vec![]
105 }
106 };
107 CommitList { commits, input }
108 }
109}
110
111impl Iterator for CommitList {
112 type Item = Commit;
113 fn next(&mut self) -> Option<Self::Item> {
114 self.commits.pop().map(Commit::from)
115 }
116}
117
118impl<'a> IntoIterator for &'a Commit {
119 type Item = Line;
120 type IntoIter = CommitMessage<'a>;
121 fn into_iter(self) -> Self::IntoIter {
122 CommitMessage(self.message.lines().collect())
123 }
124}
125
126impl<'a> Iterator for CommitMessage<'a> {
127 type Item = Line;
128 fn next(&mut self) -> Option<Self::Item> {
129 if self.0.is_empty() {
130 None
131 } else {
132 Some(parse_line(self.0.remove(0)))
133 }
134 }
135}
136
137impl fmt::Display for Commit {
138 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
139 write!(f, "{}:{}", self.sha, self.summary)
140 }
141}
142
143impl fmt::Display for CommitList {
144 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
145 write!(f, "{} ({} commits)", self.input, self.commits.len())
146 }
147}
148
149fn parse_subject(line: &str) -> String {
151 let first_open = line.find("(#").unwrap_or_else(|| line.len());
153
154 String::from(line.get(0..first_open).unwrap_or_else(|| line).trim())
156}
157
158fn parse_number(line: &str) -> Option<u32> {
160 let last_open = line.rfind("(#");
162
163 let last_close = line.rfind(')');
165
166 if last_open.is_none() || last_close.is_none() {
168 None
170 } else {
171 let end = last_close.unwrap();
173 let start = last_open.unwrap() + "(#".len();
174
175 let num = line.get(start..end).map(|s| s.parse().ok());
177
178 num.unwrap_or(None)
180 }
181}
182
183fn parse_line(line: &str) -> Line {
185 match tagged_change(line) {
187 IResult::Done(_, l) => l,
189
190 _ => Line::default(),
192 }
193}
194
195named!(tagged_change<&str, Line>,
197 alt!(with_category
198 | with_category_scope
199 | with_category_text
200 | with_category_scope_text
201 | with_text
202 ));
203
204named!(with_text<&str, Line>,
206 do_parse!(opt!(tag!("-")) >>
207 text: whatever >>
208 (Line{
209 scope: None,
210 category: None,
211 text: Some(text)
212 })));
213
214named!(with_category<&str, Line>,
216 do_parse!(
217 tag!("-") >> category: tagname >>
218 tag!(":") >> eof!() >>
219 (Line{
220 scope: None,
221 category: Some(category),
222 text: None
223 })));
224
225named!(with_category_scope<&str, Line>,
227 do_parse!(
228 tag!("-") >> category: tagname >>
229 tag!("(") >> scope: tagname >>
230 tag!("):") >> eof!() >>
231 (Line{
232 scope: Some(scope),
233 category: Some(category),
234 text: None
235 })));
236
237named!(with_category_text<&str, Line>,
239 do_parse!(
240 tag!("-") >> category: tagname >>
241 tag!(":") >> text: whatever >>
242 (Line{
243 scope: None,
244 category: Some(category),
245 text: Some(text)
246 })));
247
248named!(with_category_scope_text<&str, Line>,
250 do_parse!(
251 tag!("-") >> category: tagname >>
252 tag!("(") >> scope: tagname >>
253 tag!("):") >> text: whatever >>
254 (Line{
255 scope: Some(scope),
256 category: Some(category),
257 text: Some(text)
258 })));
259
260named!(whatever<&str, String>,
262 map!(take_while1_s!(|_| true), String::from));
263
264named!(tagname<&str, String>,
266 map!(ws!(take_while1_s!(|c| is_alphanumeric(c as u8))), str::to_lowercase));
267
268#[cfg(test)]
269mod tests {
270 #[test]
271 fn commit_fetch() {
272 use super::{Commit, CommitList};
273 let head = Commit::from("2c5dda2e");
274 let list = CommitList::from("2c5dda2e^..2c5dda2e");
275 assert_eq!(list.to_string(), "2c5dda2e^..2c5dda2e (1 commits)");
276 let also_head = list.into_iter().next().unwrap();
277 assert_eq!(head.sha, also_head.sha);
278 assert!(head.to_string().starts_with("2c5dda2e"));
279 }
280
281 #[test]
282 fn negative() {
283 assert!(super::Commit::from("no-such-commit").summary.is_empty());
284 assert_eq!(super::CommitList::from("bad-range").into_iter().count(), 0);
285 }
286
287 #[test]
288 fn commit_lines() {
289 use super::Commit;
290 let reference = &Commit::from("2c5dda2e5ec6d0ad7abdcd20661bf2cb846ee5f2");
291 assert_eq!(reference.into_iter().count(), 17);
292 }
293
294 #[test]
295 fn commit_parse_summary() {
296 use super::{parse_number, parse_subject};
297
298 let message = "foo bar (#123)";
300 assert_eq!(parse_subject(message), "foo bar");
301 assert_eq!(parse_number(message), Some(123));
302
303 let message = "foo bar ()()";
305 assert_eq!(parse_subject(message), message);
306 assert_eq!(parse_number(message), None);
307
308 let message = "foo bar #123 (#101)(#103)";
310 assert_eq!(parse_subject(message), "foo bar #123");
311 assert_eq!(parse_number(message), Some(103));
312 }
313
314 #[test]
315 fn commit_parse_line() {
316 use commit::parse_line;
317
318 let line = parse_line("- break(shell): foo bar");
319 assert_eq!(Some(String::from("shell")), line.scope);
320 assert_eq!(Some(String::from("break")), line.category);
321 assert_eq!(Some(String::from(" foo bar")), line.text);
322
323 let line = parse_line("-BREAK ( Shell ): foo bar");
324 assert_eq!(Some(String::from("shell")), line.scope);
325 assert_eq!(Some(String::from("break")), line.category);
326 assert_eq!(Some(String::from(" foo bar")), line.text);
327
328 let line = parse_line("- break(shell):");
329 assert_eq!(Some(String::from("shell")), line.scope);
330 assert_eq!(Some(String::from("break")), line.category);
331 assert_eq!(None, line.text);
332
333 let line = parse_line("- break ( SHELL ):");
334 assert_eq!(Some(String::from("shell")), line.scope);
335 assert_eq!(Some(String::from("break")), line.category);
336 assert_eq!(None, line.text);
337
338 let line = parse_line("-fix:");
339 assert_eq!(None, line.scope);
340 assert_eq!(Some(String::from("fix")), line.category);
341 assert_eq!(None, line.text);
342
343 let line = parse_line("- fix: foo bar");
344 assert_eq!(None, line.scope);
345 assert_eq!(Some(String::from("fix")), line.category);
346 assert_eq!(Some(String::from(" foo bar")), line.text);
347
348 let line = parse_line("- FIX : foo bar");
349 assert_eq!(None, line.scope);
350 assert_eq!(Some(String::from("fix")), line.category);
351 assert_eq!(Some(String::from(" foo bar")), line.text);
352
353 let line = parse_line("- foo bar");
354 assert_eq!(None, line.scope);
355 assert_eq!(None, line.category);
356 assert_eq!(Some(String::from(" foo bar")), line.text);
357
358 let line = parse_line("foo bar");
359 assert_eq!(None, line.scope);
360 assert_eq!(None, line.category);
361 assert_eq!(Some(String::from("foo bar")), line.text);
362
363 let line = parse_line("");
364 assert_eq!(None, line.text);
365 assert_eq!(None, line.scope);
366 assert_eq!(None, line.category);
367 }
368}