1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
use core::panic;
use std::{collections::HashMap, env::current_dir, fs, io::read_to_string, str::FromStr};
use git2::{Blob, Delta, DiffLineType, DiffOptions, Oid, Repository};
use crate::cli::Cli;
#[derive(Eq, PartialEq, Hash, Clone, Debug)]
pub enum FileChange {
Addition(String, Oid), // new file name
Modification(String, Oid, Oid), // old and new file names
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum EditType {
Add,
Delete,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Edit {
pub edit_type: EditType,
pub modification: String,
pub line: u32, // For deletions, this will be the location in the new file the line would have
// been
}
pub type GitChanges = HashMap<FileChange, Vec<Edit>>;
pub struct Git {
repository: Repository,
}
impl Git {
pub fn new(dir: &String) -> Option<Git> {
let repo = match Repository::open(dir) {
Ok(repo) => repo,
Err(e) => {
panic!("error loading git repository: {}", e);
return None;
}
};
Some(Git { repository: repo })
}
pub fn changes(&self, b: &String, args: &Cli) -> GitChanges {
let repo = &self.repository;
// Load the old (a) and new (b) commits
//let a_id = repo.revparse_single(a.as_str()).unwrap().id();
//let a_tree = repo.find_commit(a_id).unwrap().tree().unwrap();
//let a_id = repo.index().unwrap().write_tree().unwrap();
//let a_tree = repo.find_tree(a_id).unwrap();
let b_id = repo.revparse_single(b.as_str()).unwrap().id();
let b_tree = repo.find_commit(b_id).unwrap().tree().unwrap();
// Set the options for the diff
let mut diff_options = DiffOptions::new();
// NOTE: pathspec does not support just putting "." to get the current dir, have to work out the
// relative path
let repo_dir = repo.workdir().unwrap();
let work_dir = current_dir().unwrap();
let relative_dir = work_dir
.strip_prefix(repo_dir)
.unwrap_or(repo_dir)
.to_str()
.unwrap();
diff_options.pathspec(relative_dir);
diff_options.include_untracked(true);
diff_options.recurse_untracked_dirs(true);
//diff_options.update_index(true);
// get the diff
let diff = repo
.diff_tree_to_workdir_with_index(Some(&b_tree), Some(&mut diff_options))
.unwrap();
// Collect all the edits made to each Rust file
let mut map: HashMap<FileChange, Vec<Edit>> = HashMap::new();
let mut deltas: HashMap<FileChange, i128> = HashMap::new();
// NOTE: to correctly handle the delta, this expects that the edits in each file come in
// order top to bottom
let _ = diff.foreach(
&mut |_, _| true,
None,
None,
Some(&mut |a, b, c| {
let path = a.new_file().path().unwrap().to_str().unwrap();
if !path.ends_with(".rs")
{
let warn = match &args.ignore {
Some(ignore) => {
!ignore.into_iter().any(|p| path.starts_with(format!("{}/{}", relative_dir, p).as_str()))
},
None => true
};
if warn {
println!("NOTE: skipping file \"{}\" as not Rust code. If this file affects the program this will be undetected {}", path, a.new_file().path().unwrap().file_name().unwrap().to_string_lossy());
}
return true;
}
// Hack to deal with objects in working dir. Instead it should be refactored to the DiffDelta is stored in FileChange and get_file reads from disk instead of using the ODB
let mut dir = std::env::current_dir().unwrap();
dir.push(&args.git_dir);
if self.repository.find_blob(a.new_file().id()).is_err() {
dir.push(a.new_file().path().unwrap());
repo.blob(fs::read(dir).unwrap().as_slice()).unwrap();
}
// Get the type of change the file had
// NOTE: only tracks files that are added or modified
let change = match a.status() {
Delta::Added => Some(FileChange::Addition(
String::from_str(a.new_file().path().unwrap().to_str().unwrap()).unwrap(),
a.new_file().id(),
)),
// TODO: treat renamed and copied as a Modification or Addition???
Delta::Modified | Delta::Renamed | Delta::Copied => Some(FileChange::Modification(
String::from_str(a.new_file().path().unwrap().to_str().unwrap()).unwrap(),
a.old_file().id(),
a.new_file().id(),
)),
status => {
eprintln!("unhandled change of type: {:#?}", status);
None
}
};
// Ignore unsupported changes
let change = match change {
Some(change) => change,
None => return true,
};
// Set default for the key if doesn't exist
map.entry(change.clone()).or_insert(Vec::new());
let delta = deltas.entry(change.clone()).or_insert(0);
// TODO: combine lines of edits into blocks like jgit ?
match &c.origin_value() {
DiffLineType::Context => {
// TODO: start a new edit?
}
DiffLineType::Addition => {
let new = &c.new_lineno().unwrap();
let edit = Edit {
edit_type: EditType::Add,
line: *new,
modification: String::from_utf8(c.content().to_vec()).unwrap(),
};
map.get_mut(&change).unwrap().push(edit);
*delta += 1;
}
DiffLineType::Deletion => {
let old = &c.old_lineno().unwrap();
//println!("old: {}, new: {}", old, *old as i128+delta.clone());
let edit = Edit {
edit_type: EditType::Delete,
// Calculate where the deleted line _would be_ in the new file, based
// on the lines added and deleted before it
line: (*old as i128+delta.clone()) as u32,
modification: String::from_utf8(c.content().to_vec()).unwrap(),
};
map.get_mut(&change).unwrap().push(edit);
*delta -= 1;
}
_ => {
eprintln!("UNEXPECTED ORIGIN: '{}'", &c.origin())
}
}
true
}),
);
return map;
}
pub fn get_file(&self, oid: Oid) -> String {
//let file =
//String::from_utf8(self.repository.find_blob(oid).unwrap().content().to_vec()).unwrap();
//dbg!(oid);
let file = String::from_utf8(
self.repository
.find_object(oid, Some(git2::ObjectType::Blob))
.unwrap()
.into_blob()
.unwrap()
.content()
.to_vec(),
)
.unwrap();
return file;
}
}
#[cfg(test)]
mod test {
#[test]
fn test1() {
//Git::diff(
//&"35f43c".to_string(),
//&"eaf59e".to_string(),
//&"..".to_string(),
//);
}
}