1use std::io::{self, BufReader, BufRead};
11use std::process::{Child, Command, Stdio};
12use std::sync::mpsc::SyncSender;
13
14use time::Timespec;
15
16use types::*;
17
18#[derive(Debug, Clone)]
20pub struct ParsedCommit {
21 pub id: SHA1,
22 pub when: Timespec,
24 pub deltas: Vec<FileDelta>,
25}
26
27impl Default for ParsedCommit {
28 fn default() -> ParsedCommit
29 {
30 ParsedCommit {
31 id: SHA1::default(),
32 when: Timespec::new(0, 0),
33 deltas: Vec::new()
34 }
35 }
36}
37
38fn start_history_process() -> Result<Child, io::Error> {
40 Command::new("git")
41 .arg("log")
42 .arg("--name-status")
43 .arg("-M")
44 .arg("-C")
45 .arg("--pretty=format:%H%n%at") .stdout(Stdio::piped())
47 .spawn()
48}
49
50pub fn get_history(sink: &SyncSender<ParsedCommit>) {
55
56 enum ParseState { Hash,
58 Timestamp,
59 Changes
60 }
61
62 let child = start_history_process().expect("Couldn't open repo history");
63 let br = BufReader::new(child.stdout.unwrap());
64
65 let mut state = ParseState::Hash;
66 let mut current_commit = ParsedCommit::default();
67
68 for line in br.lines().map(|l| l.unwrap()) {
69
70 if line.is_empty() { continue; } let next_state;
73 match state {
74 ParseState::Hash => {
75 current_commit.id = SHA1::parse(&line).unwrap();
76 next_state = ParseState::Timestamp;
77 }
78
79 ParseState::Timestamp => {
80 current_commit.when = Timespec{ sec: line.parse().unwrap(),
81 nsec: 0 };
82 next_state = ParseState::Changes;
83 }
84
85 ParseState::Changes => {
86 if let Ok(id) = SHA1::parse(&line) {
88 commit_sink(current_commit, sink);
89 current_commit = ParsedCommit::default();
90
91 current_commit.id = id;
94 next_state = ParseState::Timestamp;
95 }
96 else {
97 next_state = state;
99
100 current_commit.deltas.push(parse_delta(&line));
101 }
102 }
103 }
104 state = next_state;
105 }
106
107 commit_sink(current_commit, sink);
109}
110
111#[inline]
113fn commit_sink(c: ParsedCommit, sink: &SyncSender<ParsedCommit>) {
114 sink.send(c).expect("The other end stopped listening for commits.");
115}
116
117fn parse_delta(s: &str) -> FileDelta {
119 let tokens : Vec<&str> = s.split('\t').collect();
120
121 assert!(tokens.len() > 1, "Expected at least one token");
122 let c = parse_change_code(tokens[0]);
123 let previous : String;
124 let current : String;
125
126 match c {
127 Change::Renamed { .. } |
128 Change::Copied { .. }=> {
129 assert_eq!(tokens.len(), 3, "Expected three tokens from string {:?}", s);
130 current = tokens[2].to_string();
131 previous = tokens[1].to_string();
132 }
133
134 _ => {
135 assert_eq!(tokens.len(), 2, "Expected two tokens from string {:?}", s);
136 current = tokens[1].to_string();
137 previous = String::new();
138 }
139 };
140
141 FileDelta{ change: c, path: current, from: previous }
142}
143
144fn parse_change_code(c: &str) -> Change {
146 assert!(!c.is_empty());
147 let ret = match c.chars().nth(0).unwrap() {
148 'A' => Change::Added,
149 'D' => Change::Deleted,
150 'M' |
151 'T' => Change::Modified, 'R' => Change::Renamed{ percent_changed: c[1..].parse().unwrap() },
154 'C' => Change::Copied{ percent_changed: c[1..].parse().unwrap() },
155 _ => panic!("Unknown delta code: {}", c)
156 };
157
158 match ret {
160 Change::Renamed{ percent_changed: r} => { assert!(r <= 100); },
161 Change::Copied{ percent_changed: c} => { assert!(c <= 100); },
162 _ => { }
163 };
164
165 ret
166}