1 use crate::util::{self, Binding};
2 use crate::{raw, signature, Oid, Repository, Signature};
3 use std::marker;
4 use std::mem;
5 use std::ops::Range;
6 use std::path::Path;
7 
8 /// Opaque structure to hold blame results.
9 pub struct Blame<'repo> {
10     raw: *mut raw::git_blame,
11     _marker: marker::PhantomData<&'repo Repository>,
12 }
13 
14 /// Structure that represents a blame hunk.
15 pub struct BlameHunk<'blame> {
16     raw: *mut raw::git_blame_hunk,
17     _marker: marker::PhantomData<&'blame raw::git_blame>,
18 }
19 
20 /// Blame options
21 pub struct BlameOptions {
22     raw: raw::git_blame_options,
23 }
24 
25 /// An iterator over the hunks in a blame.
26 pub struct BlameIter<'blame> {
27     range: Range<usize>,
28     blame: &'blame Blame<'blame>,
29 }
30 
31 impl<'repo> Blame<'repo> {
32     /// Gets the number of hunks that exist in the blame structure.
len(&self) -> usize33     pub fn len(&self) -> usize {
34         unsafe { raw::git_blame_get_hunk_count(self.raw) as usize }
35     }
36 
37     /// Return `true` is there is no hunk in the blame structure.
is_empty(&self) -> bool38     pub fn is_empty(&self) -> bool {
39         self.len() == 0
40     }
41 
42     /// Gets the blame hunk at the given index.
get_index(&self, index: usize) -> Option<BlameHunk<'_>>43     pub fn get_index(&self, index: usize) -> Option<BlameHunk<'_>> {
44         unsafe {
45             let ptr = raw::git_blame_get_hunk_byindex(self.raw(), index as u32);
46             if ptr.is_null() {
47                 None
48             } else {
49                 Some(BlameHunk::from_raw_const(ptr))
50             }
51         }
52     }
53 
54     /// Gets the hunk that relates to the given line number in the newest
55     /// commit.
get_line(&self, lineno: usize) -> Option<BlameHunk<'_>>56     pub fn get_line(&self, lineno: usize) -> Option<BlameHunk<'_>> {
57         unsafe {
58             let ptr = raw::git_blame_get_hunk_byline(self.raw(), lineno);
59             if ptr.is_null() {
60                 None
61             } else {
62                 Some(BlameHunk::from_raw_const(ptr))
63             }
64         }
65     }
66 
67     /// Returns an iterator over the hunks in this blame.
iter(&self) -> BlameIter<'_>68     pub fn iter(&self) -> BlameIter<'_> {
69         BlameIter {
70             range: 0..self.len(),
71             blame: self,
72         }
73     }
74 }
75 
76 impl<'blame> BlameHunk<'blame> {
from_raw_const(raw: *const raw::git_blame_hunk) -> BlameHunk<'blame>77     unsafe fn from_raw_const(raw: *const raw::git_blame_hunk) -> BlameHunk<'blame> {
78         BlameHunk {
79             raw: raw as *mut raw::git_blame_hunk,
80             _marker: marker::PhantomData,
81         }
82     }
83 
84     /// Returns OID of the commit where this line was last changed
final_commit_id(&self) -> Oid85     pub fn final_commit_id(&self) -> Oid {
86         unsafe { Oid::from_raw(&(*self.raw).final_commit_id) }
87     }
88 
89     /// Returns signature of the commit.
final_signature(&self) -> Signature<'_>90     pub fn final_signature(&self) -> Signature<'_> {
91         unsafe { signature::from_raw_const(self, (*self.raw).final_signature) }
92     }
93 
94     /// Returns line number where this hunk begins.
95     ///
96     /// Note that the start line is counting from 1.
final_start_line(&self) -> usize97     pub fn final_start_line(&self) -> usize {
98         unsafe { (*self.raw).final_start_line_number }
99     }
100 
101     /// Returns the OID of the commit where this hunk was found.
102     ///
103     /// This will usually be the same as `final_commit_id`,
104     /// except when `BlameOptions::track_copies_any_commit_copies` has been
105     /// turned on
orig_commit_id(&self) -> Oid106     pub fn orig_commit_id(&self) -> Oid {
107         unsafe { Oid::from_raw(&(*self.raw).orig_commit_id) }
108     }
109 
110     /// Returns signature of the commit.
orig_signature(&self) -> Signature<'_>111     pub fn orig_signature(&self) -> Signature<'_> {
112         unsafe { signature::from_raw_const(self, (*self.raw).orig_signature) }
113     }
114 
115     /// Returns line number where this hunk begins.
116     ///
117     /// Note that the start line is counting from 1.
orig_start_line(&self) -> usize118     pub fn orig_start_line(&self) -> usize {
119         unsafe { (*self.raw).orig_start_line_number }
120     }
121 
122     /// Returns path to the file where this hunk originated.
123     ///
124     /// Note: `None` could be returned for non-unicode paths on Widnows.
path(&self) -> Option<&Path>125     pub fn path(&self) -> Option<&Path> {
126         unsafe {
127             if let Some(bytes) = crate::opt_bytes(self, (*self.raw).orig_path) {
128                 Some(util::bytes2path(bytes))
129             } else {
130                 None
131             }
132         }
133     }
134 
135     /// Tests whether this hunk has been tracked to a boundary commit
136     /// (the root, or the commit specified in git_blame_options.oldest_commit).
is_boundary(&self) -> bool137     pub fn is_boundary(&self) -> bool {
138         unsafe { (*self.raw).boundary == 1 }
139     }
140 
141     /// Returns number of lines in this hunk.
lines_in_hunk(&self) -> usize142     pub fn lines_in_hunk(&self) -> usize {
143         unsafe { (*self.raw).lines_in_hunk as usize }
144     }
145 }
146 
147 impl Default for BlameOptions {
default() -> Self148     fn default() -> Self {
149         Self::new()
150     }
151 }
152 
153 impl BlameOptions {
154     /// Initialize options
new() -> BlameOptions155     pub fn new() -> BlameOptions {
156         unsafe {
157             let mut raw: raw::git_blame_options = mem::zeroed();
158             assert_eq!(
159                 raw::git_blame_init_options(&mut raw, raw::GIT_BLAME_OPTIONS_VERSION),
160                 0
161             );
162 
163             Binding::from_raw(&raw as *const _ as *mut _)
164         }
165     }
166 
flag(&mut self, opt: u32, val: bool) -> &mut BlameOptions167     fn flag(&mut self, opt: u32, val: bool) -> &mut BlameOptions {
168         if val {
169             self.raw.flags |= opt;
170         } else {
171             self.raw.flags &= !opt;
172         }
173         self
174     }
175 
176     /// Track lines that have moved within a file.
track_copies_same_file(&mut self, opt: bool) -> &mut BlameOptions177     pub fn track_copies_same_file(&mut self, opt: bool) -> &mut BlameOptions {
178         self.flag(raw::GIT_BLAME_TRACK_COPIES_SAME_FILE, opt)
179     }
180 
181     /// Track lines that have moved across files in the same commit.
track_copies_same_commit_moves(&mut self, opt: bool) -> &mut BlameOptions182     pub fn track_copies_same_commit_moves(&mut self, opt: bool) -> &mut BlameOptions {
183         self.flag(raw::GIT_BLAME_TRACK_COPIES_SAME_COMMIT_MOVES, opt)
184     }
185 
186     /// Track lines that have been copied from another file that exists
187     /// in the same commit.
track_copies_same_commit_copies(&mut self, opt: bool) -> &mut BlameOptions188     pub fn track_copies_same_commit_copies(&mut self, opt: bool) -> &mut BlameOptions {
189         self.flag(raw::GIT_BLAME_TRACK_COPIES_SAME_COMMIT_COPIES, opt)
190     }
191 
192     /// Track lines that have been copied from another file that exists
193     /// in any commit.
track_copies_any_commit_copies(&mut self, opt: bool) -> &mut BlameOptions194     pub fn track_copies_any_commit_copies(&mut self, opt: bool) -> &mut BlameOptions {
195         self.flag(raw::GIT_BLAME_TRACK_COPIES_ANY_COMMIT_COPIES, opt)
196     }
197 
198     /// Restrict the search of commits to those reachable following only
199     /// the first parents.
first_parent(&mut self, opt: bool) -> &mut BlameOptions200     pub fn first_parent(&mut self, opt: bool) -> &mut BlameOptions {
201         self.flag(raw::GIT_BLAME_FIRST_PARENT, opt)
202     }
203 
204     /// Use mailmap file to map author and committer names and email addresses
205     /// to canonical real names and email addresses. The mailmap will be read
206     /// from the working directory, or HEAD in a bare repository.
use_mailmap(&mut self, opt: bool) -> &mut BlameOptions207     pub fn use_mailmap(&mut self, opt: bool) -> &mut BlameOptions {
208         self.flag(raw::GIT_BLAME_USE_MAILMAP, opt)
209     }
210 
211     /// Ignore whitespace differences.
ignore_whitespace(&mut self, opt: bool) -> &mut BlameOptions212     pub fn ignore_whitespace(&mut self, opt: bool) -> &mut BlameOptions {
213         self.flag(raw::GIT_BLAME_IGNORE_WHITESPACE, opt)
214     }
215 
216     /// Setter for the id of the newest commit to consider.
newest_commit(&mut self, id: Oid) -> &mut BlameOptions217     pub fn newest_commit(&mut self, id: Oid) -> &mut BlameOptions {
218         unsafe {
219             self.raw.newest_commit = *id.raw();
220         }
221         self
222     }
223 
224     /// Setter for the id of the oldest commit to consider.
oldest_commit(&mut self, id: Oid) -> &mut BlameOptions225     pub fn oldest_commit(&mut self, id: Oid) -> &mut BlameOptions {
226         unsafe {
227             self.raw.oldest_commit = *id.raw();
228         }
229         self
230     }
231 
232     /// The first line in the file to blame.
min_line(&mut self, lineno: usize) -> &mut BlameOptions233     pub fn min_line(&mut self, lineno: usize) -> &mut BlameOptions {
234         self.raw.min_line = lineno;
235         self
236     }
237 
238     /// The last line in the file to blame.
max_line(&mut self, lineno: usize) -> &mut BlameOptions239     pub fn max_line(&mut self, lineno: usize) -> &mut BlameOptions {
240         self.raw.max_line = lineno;
241         self
242     }
243 }
244 
245 impl<'repo> Binding for Blame<'repo> {
246     type Raw = *mut raw::git_blame;
247 
from_raw(raw: *mut raw::git_blame) -> Blame<'repo>248     unsafe fn from_raw(raw: *mut raw::git_blame) -> Blame<'repo> {
249         Blame {
250             raw,
251             _marker: marker::PhantomData,
252         }
253     }
254 
raw(&self) -> *mut raw::git_blame255     fn raw(&self) -> *mut raw::git_blame {
256         self.raw
257     }
258 }
259 
260 impl<'repo> Drop for Blame<'repo> {
drop(&mut self)261     fn drop(&mut self) {
262         unsafe { raw::git_blame_free(self.raw) }
263     }
264 }
265 
266 impl<'blame> Binding for BlameHunk<'blame> {
267     type Raw = *mut raw::git_blame_hunk;
268 
from_raw(raw: *mut raw::git_blame_hunk) -> BlameHunk<'blame>269     unsafe fn from_raw(raw: *mut raw::git_blame_hunk) -> BlameHunk<'blame> {
270         BlameHunk {
271             raw,
272             _marker: marker::PhantomData,
273         }
274     }
275 
raw(&self) -> *mut raw::git_blame_hunk276     fn raw(&self) -> *mut raw::git_blame_hunk {
277         self.raw
278     }
279 }
280 
281 impl Binding for BlameOptions {
282     type Raw = *mut raw::git_blame_options;
283 
from_raw(opts: *mut raw::git_blame_options) -> BlameOptions284     unsafe fn from_raw(opts: *mut raw::git_blame_options) -> BlameOptions {
285         BlameOptions { raw: *opts }
286     }
287 
raw(&self) -> *mut raw::git_blame_options288     fn raw(&self) -> *mut raw::git_blame_options {
289         &self.raw as *const _ as *mut _
290     }
291 }
292 
293 impl<'blame> Iterator for BlameIter<'blame> {
294     type Item = BlameHunk<'blame>;
next(&mut self) -> Option<BlameHunk<'blame>>295     fn next(&mut self) -> Option<BlameHunk<'blame>> {
296         self.range.next().and_then(|i| self.blame.get_index(i))
297     }
298 
size_hint(&self) -> (usize, Option<usize>)299     fn size_hint(&self) -> (usize, Option<usize>) {
300         self.range.size_hint()
301     }
302 }
303 
304 impl<'blame> DoubleEndedIterator for BlameIter<'blame> {
next_back(&mut self) -> Option<BlameHunk<'blame>>305     fn next_back(&mut self) -> Option<BlameHunk<'blame>> {
306         self.range.next_back().and_then(|i| self.blame.get_index(i))
307     }
308 }
309 
310 impl<'blame> ExactSizeIterator for BlameIter<'blame> {}
311 
312 #[cfg(test)]
313 mod tests {
314     use std::fs::{self, File};
315     use std::path::Path;
316 
317     #[test]
smoke()318     fn smoke() {
319         let (_td, repo) = crate::test::repo_init();
320         let mut index = repo.index().unwrap();
321 
322         let root = repo.workdir().unwrap();
323         fs::create_dir(&root.join("foo")).unwrap();
324         File::create(&root.join("foo/bar")).unwrap();
325         index.add_path(Path::new("foo/bar")).unwrap();
326 
327         let id = index.write_tree().unwrap();
328         let tree = repo.find_tree(id).unwrap();
329         let sig = repo.signature().unwrap();
330         let id = repo.refname_to_id("HEAD").unwrap();
331         let parent = repo.find_commit(id).unwrap();
332         let commit = repo
333             .commit(Some("HEAD"), &sig, &sig, "commit", &tree, &[&parent])
334             .unwrap();
335 
336         let blame = repo.blame_file(Path::new("foo/bar"), None).unwrap();
337 
338         assert_eq!(blame.len(), 1);
339         assert_eq!(blame.iter().count(), 1);
340 
341         let hunk = blame.get_index(0).unwrap();
342         assert_eq!(hunk.final_commit_id(), commit);
343         assert_eq!(hunk.final_signature().name(), sig.name());
344         assert_eq!(hunk.final_signature().email(), sig.email());
345         assert_eq!(hunk.final_start_line(), 1);
346         assert_eq!(hunk.path(), Some(Path::new("foo/bar")));
347         assert_eq!(hunk.lines_in_hunk(), 0);
348         assert!(!hunk.is_boundary())
349     }
350 }
351