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: 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: 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