[go: up one dir, main page]

git2/
blame.rs

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