1 use libc::{c_char, c_uint, size_t}; 2 use std::ffi::CString; 3 use std::marker; 4 use std::mem; 5 use std::ops::Range; 6 use std::str; 7 8 use crate::util::{self, Binding}; 9 use crate::{raw, DiffDelta, IntoCString, Repository, Status}; 10 11 /// Options that can be provided to `repo.statuses()` to control how the status 12 /// information is gathered. 13 pub struct StatusOptions { 14 raw: raw::git_status_options, 15 pathspec: Vec<CString>, 16 ptrs: Vec<*const c_char>, 17 } 18 19 /// Enumeration of possible methods of what can be shown through a status 20 /// operation. 21 #[derive(Copy, Clone)] 22 pub enum StatusShow { 23 /// Only gives status based on HEAD to index comparison, not looking at 24 /// working directory changes. 25 Index, 26 27 /// Only gives status based on index to working directory comparison, not 28 /// comparing the index to the HEAD. 29 Workdir, 30 31 /// The default, this roughly matches `git status --porcelain` regarding 32 /// which files are included and in what order. 33 IndexAndWorkdir, 34 } 35 36 /// A container for a list of status information about a repository. 37 /// 38 /// Each instance appears as if it were a collection, having a length and 39 /// allowing indexing, as well as providing an iterator. 40 pub struct Statuses<'repo> { 41 raw: *mut raw::git_status_list, 42 43 // Hm, not currently present, but can't hurt? 44 _marker: marker::PhantomData<&'repo Repository>, 45 } 46 47 /// An iterator over the statuses in a `Statuses` instance. 48 pub struct StatusIter<'statuses> { 49 statuses: &'statuses Statuses<'statuses>, 50 range: Range<usize>, 51 } 52 53 /// A structure representing an entry in the `Statuses` structure. 54 /// 55 /// Instances are created through the `.iter()` method or the `.get()` method. 56 pub struct StatusEntry<'statuses> { 57 raw: *const raw::git_status_entry, 58 _marker: marker::PhantomData<&'statuses DiffDelta<'statuses>>, 59 } 60 61 impl Default for StatusOptions { default() -> Self62 fn default() -> Self { 63 Self::new() 64 } 65 } 66 67 impl StatusOptions { 68 /// Creates a new blank set of status options. new() -> StatusOptions69 pub fn new() -> StatusOptions { 70 unsafe { 71 let mut raw = mem::zeroed(); 72 let r = raw::git_status_init_options(&mut raw, raw::GIT_STATUS_OPTIONS_VERSION); 73 assert_eq!(r, 0); 74 StatusOptions { 75 raw: raw, 76 pathspec: Vec::new(), 77 ptrs: Vec::new(), 78 } 79 } 80 } 81 82 /// Select the files on which to report status. 83 /// 84 /// The default, if unspecified, is to show the index and the working 85 /// directory. show(&mut self, show: StatusShow) -> &mut StatusOptions86 pub fn show(&mut self, show: StatusShow) -> &mut StatusOptions { 87 self.raw.show = match show { 88 StatusShow::Index => raw::GIT_STATUS_SHOW_INDEX_ONLY, 89 StatusShow::Workdir => raw::GIT_STATUS_SHOW_WORKDIR_ONLY, 90 StatusShow::IndexAndWorkdir => raw::GIT_STATUS_SHOW_INDEX_AND_WORKDIR, 91 }; 92 self 93 } 94 95 /// Add a path pattern to match (using fnmatch-style matching). 96 /// 97 /// If the `disable_pathspec_match` option is given, then this is a literal 98 /// path to match. If this is not called, then there will be no patterns to 99 /// match and the entire directory will be used. pathspec<T: IntoCString>(&mut self, pathspec: T) -> &mut StatusOptions100 pub fn pathspec<T: IntoCString>(&mut self, pathspec: T) -> &mut StatusOptions { 101 let s = util::cstring_to_repo_path(pathspec).unwrap(); 102 self.ptrs.push(s.as_ptr()); 103 self.pathspec.push(s); 104 self 105 } 106 flag(&mut self, flag: raw::git_status_opt_t, val: bool) -> &mut StatusOptions107 fn flag(&mut self, flag: raw::git_status_opt_t, val: bool) -> &mut StatusOptions { 108 if val { 109 self.raw.flags |= flag as c_uint; 110 } else { 111 self.raw.flags &= !(flag as c_uint); 112 } 113 self 114 } 115 116 /// Flag whether untracked files will be included. 117 /// 118 /// Untracked files will only be included if the workdir files are included 119 /// in the status "show" option. include_untracked(&mut self, include: bool) -> &mut StatusOptions120 pub fn include_untracked(&mut self, include: bool) -> &mut StatusOptions { 121 self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNTRACKED, include) 122 } 123 124 /// Flag whether ignored files will be included. 125 /// 126 /// The files will only be included if the workdir files are included 127 /// in the status "show" option. include_ignored(&mut self, include: bool) -> &mut StatusOptions128 pub fn include_ignored(&mut self, include: bool) -> &mut StatusOptions { 129 self.flag(raw::GIT_STATUS_OPT_INCLUDE_IGNORED, include) 130 } 131 132 /// Flag to include unmodified files. include_unmodified(&mut self, include: bool) -> &mut StatusOptions133 pub fn include_unmodified(&mut self, include: bool) -> &mut StatusOptions { 134 self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNMODIFIED, include) 135 } 136 137 /// Flag that submodules should be skipped. 138 /// 139 /// This only applies if there are no pending typechanges to the submodule 140 /// (either from or to another type). exclude_submodules(&mut self, exclude: bool) -> &mut StatusOptions141 pub fn exclude_submodules(&mut self, exclude: bool) -> &mut StatusOptions { 142 self.flag(raw::GIT_STATUS_OPT_EXCLUDE_SUBMODULES, exclude) 143 } 144 145 /// Flag that all files in untracked directories should be included. 146 /// 147 /// Normally if an entire directory is new then just the top-level directory 148 /// is included (with a trailing slash on the entry name). recurse_untracked_dirs(&mut self, include: bool) -> &mut StatusOptions149 pub fn recurse_untracked_dirs(&mut self, include: bool) -> &mut StatusOptions { 150 self.flag(raw::GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS, include) 151 } 152 153 /// Indicates that the given paths should be treated as literals paths, note 154 /// patterns. disable_pathspec_match(&mut self, include: bool) -> &mut StatusOptions155 pub fn disable_pathspec_match(&mut self, include: bool) -> &mut StatusOptions { 156 self.flag(raw::GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH, include) 157 } 158 159 /// Indicates that the contents of ignored directories should be included in 160 /// the status. recurse_ignored_dirs(&mut self, include: bool) -> &mut StatusOptions161 pub fn recurse_ignored_dirs(&mut self, include: bool) -> &mut StatusOptions { 162 self.flag(raw::GIT_STATUS_OPT_RECURSE_IGNORED_DIRS, include) 163 } 164 165 /// Indicates that rename detection should be processed between the head. renames_head_to_index(&mut self, include: bool) -> &mut StatusOptions166 pub fn renames_head_to_index(&mut self, include: bool) -> &mut StatusOptions { 167 self.flag(raw::GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX, include) 168 } 169 170 /// Indicates that rename detection should be run between the index and the 171 /// working directory. renames_index_to_workdir(&mut self, include: bool) -> &mut StatusOptions172 pub fn renames_index_to_workdir(&mut self, include: bool) -> &mut StatusOptions { 173 self.flag(raw::GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR, include) 174 } 175 176 /// Override the native case sensitivity for the file system and force the 177 /// output to be in case sensitive order. sort_case_sensitively(&mut self, include: bool) -> &mut StatusOptions178 pub fn sort_case_sensitively(&mut self, include: bool) -> &mut StatusOptions { 179 self.flag(raw::GIT_STATUS_OPT_SORT_CASE_SENSITIVELY, include) 180 } 181 182 /// Override the native case sensitivity for the file system and force the 183 /// output to be in case-insensitive order. sort_case_insensitively(&mut self, include: bool) -> &mut StatusOptions184 pub fn sort_case_insensitively(&mut self, include: bool) -> &mut StatusOptions { 185 self.flag(raw::GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY, include) 186 } 187 188 /// Indicates that rename detection should include rewritten files. renames_from_rewrites(&mut self, include: bool) -> &mut StatusOptions189 pub fn renames_from_rewrites(&mut self, include: bool) -> &mut StatusOptions { 190 self.flag(raw::GIT_STATUS_OPT_RENAMES_FROM_REWRITES, include) 191 } 192 193 /// Bypasses the default status behavior of doing a "soft" index reload. no_refresh(&mut self, include: bool) -> &mut StatusOptions194 pub fn no_refresh(&mut self, include: bool) -> &mut StatusOptions { 195 self.flag(raw::GIT_STATUS_OPT_NO_REFRESH, include) 196 } 197 198 /// Refresh the stat cache in the index for files are unchanged but have 199 /// out of date stat information in the index. 200 /// 201 /// This will result in less work being done on subsequent calls to fetching 202 /// the status. update_index(&mut self, include: bool) -> &mut StatusOptions203 pub fn update_index(&mut self, include: bool) -> &mut StatusOptions { 204 self.flag(raw::GIT_STATUS_OPT_UPDATE_INDEX, include) 205 } 206 207 // erm... 208 #[allow(missing_docs)] include_unreadable(&mut self, include: bool) -> &mut StatusOptions209 pub fn include_unreadable(&mut self, include: bool) -> &mut StatusOptions { 210 self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNREADABLE, include) 211 } 212 213 // erm... 214 #[allow(missing_docs)] include_unreadable_as_untracked(&mut self, include: bool) -> &mut StatusOptions215 pub fn include_unreadable_as_untracked(&mut self, include: bool) -> &mut StatusOptions { 216 self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNREADABLE_AS_UNTRACKED, include) 217 } 218 219 /// Get a pointer to the inner list of status options. 220 /// 221 /// This function is unsafe as the returned structure has interior pointers 222 /// and may no longer be valid if these options continue to be mutated. raw(&mut self) -> *const raw::git_status_options223 pub unsafe fn raw(&mut self) -> *const raw::git_status_options { 224 self.raw.pathspec.strings = self.ptrs.as_ptr() as *mut _; 225 self.raw.pathspec.count = self.ptrs.len() as size_t; 226 &self.raw 227 } 228 } 229 230 impl<'repo> Statuses<'repo> { 231 /// Gets a status entry from this list at the specified index. 232 /// 233 /// Returns `None` if the index is out of bounds. get(&self, index: usize) -> Option<StatusEntry<'_>>234 pub fn get(&self, index: usize) -> Option<StatusEntry<'_>> { 235 unsafe { 236 let p = raw::git_status_byindex(self.raw, index as size_t); 237 Binding::from_raw_opt(p) 238 } 239 } 240 241 /// Gets the count of status entries in this list. 242 /// 243 /// If there are no changes in status (according to the options given 244 /// when the status list was created), this should return 0. len(&self) -> usize245 pub fn len(&self) -> usize { 246 unsafe { raw::git_status_list_entrycount(self.raw) as usize } 247 } 248 249 /// Return `true` if there is no status entry in this list. is_empty(&self) -> bool250 pub fn is_empty(&self) -> bool { 251 self.len() == 0 252 } 253 254 /// Returns an iterator over the statuses in this list. iter(&self) -> StatusIter<'_>255 pub fn iter(&self) -> StatusIter<'_> { 256 StatusIter { 257 statuses: self, 258 range: 0..self.len(), 259 } 260 } 261 } 262 263 impl<'repo> Binding for Statuses<'repo> { 264 type Raw = *mut raw::git_status_list; from_raw(raw: *mut raw::git_status_list) -> Statuses<'repo>265 unsafe fn from_raw(raw: *mut raw::git_status_list) -> Statuses<'repo> { 266 Statuses { 267 raw: raw, 268 _marker: marker::PhantomData, 269 } 270 } raw(&self) -> *mut raw::git_status_list271 fn raw(&self) -> *mut raw::git_status_list { 272 self.raw 273 } 274 } 275 276 impl<'repo> Drop for Statuses<'repo> { drop(&mut self)277 fn drop(&mut self) { 278 unsafe { 279 raw::git_status_list_free(self.raw); 280 } 281 } 282 } 283 284 impl<'a> Iterator for StatusIter<'a> { 285 type Item = StatusEntry<'a>; next(&mut self) -> Option<StatusEntry<'a>>286 fn next(&mut self) -> Option<StatusEntry<'a>> { 287 self.range.next().and_then(|i| self.statuses.get(i)) 288 } size_hint(&self) -> (usize, Option<usize>)289 fn size_hint(&self) -> (usize, Option<usize>) { 290 self.range.size_hint() 291 } 292 } 293 impl<'a> DoubleEndedIterator for StatusIter<'a> { next_back(&mut self) -> Option<StatusEntry<'a>>294 fn next_back(&mut self) -> Option<StatusEntry<'a>> { 295 self.range.next_back().and_then(|i| self.statuses.get(i)) 296 } 297 } 298 impl<'a> ExactSizeIterator for StatusIter<'a> {} 299 300 impl<'statuses> StatusEntry<'statuses> { 301 /// Access the bytes for this entry's corresponding pathname path_bytes(&self) -> &[u8]302 pub fn path_bytes(&self) -> &[u8] { 303 unsafe { 304 if (*self.raw).head_to_index.is_null() { 305 crate::opt_bytes(self, (*(*self.raw).index_to_workdir).old_file.path) 306 } else { 307 crate::opt_bytes(self, (*(*self.raw).head_to_index).old_file.path) 308 } 309 .unwrap() 310 } 311 } 312 313 /// Access this entry's path name as a string. 314 /// 315 /// Returns `None` if the path is not valid utf-8. path(&self) -> Option<&str>316 pub fn path(&self) -> Option<&str> { 317 str::from_utf8(self.path_bytes()).ok() 318 } 319 320 /// Access the status flags for this file status(&self) -> Status321 pub fn status(&self) -> Status { 322 Status::from_bits_truncate(unsafe { (*self.raw).status as u32 }) 323 } 324 325 /// Access detailed information about the differences between the file in 326 /// HEAD and the file in the index. head_to_index(&self) -> Option<DiffDelta<'statuses>>327 pub fn head_to_index(&self) -> Option<DiffDelta<'statuses>> { 328 unsafe { Binding::from_raw_opt((*self.raw).head_to_index) } 329 } 330 331 /// Access detailed information about the differences between the file in 332 /// the index and the file in the working directory. index_to_workdir(&self) -> Option<DiffDelta<'statuses>>333 pub fn index_to_workdir(&self) -> Option<DiffDelta<'statuses>> { 334 unsafe { Binding::from_raw_opt((*self.raw).index_to_workdir) } 335 } 336 } 337 338 impl<'statuses> Binding for StatusEntry<'statuses> { 339 type Raw = *const raw::git_status_entry; 340 from_raw(raw: *const raw::git_status_entry) -> StatusEntry<'statuses>341 unsafe fn from_raw(raw: *const raw::git_status_entry) -> StatusEntry<'statuses> { 342 StatusEntry { 343 raw: raw, 344 _marker: marker::PhantomData, 345 } 346 } raw(&self) -> *const raw::git_status_entry347 fn raw(&self) -> *const raw::git_status_entry { 348 self.raw 349 } 350 } 351 352 #[cfg(test)] 353 mod tests { 354 use super::StatusOptions; 355 use std::fs::File; 356 use std::io::prelude::*; 357 use std::path::Path; 358 359 #[test] smoke()360 fn smoke() { 361 let (td, repo) = crate::test::repo_init(); 362 assert_eq!(repo.statuses(None).unwrap().len(), 0); 363 File::create(&td.path().join("foo")).unwrap(); 364 let statuses = repo.statuses(None).unwrap(); 365 assert_eq!(statuses.iter().count(), 1); 366 let status = statuses.iter().next().unwrap(); 367 assert_eq!(status.path(), Some("foo")); 368 assert!(status.status().contains(crate::Status::WT_NEW)); 369 assert!(!status.status().contains(crate::Status::INDEX_NEW)); 370 assert!(status.head_to_index().is_none()); 371 let diff = status.index_to_workdir().unwrap(); 372 assert_eq!(diff.old_file().path_bytes().unwrap(), b"foo"); 373 assert_eq!(diff.new_file().path_bytes().unwrap(), b"foo"); 374 } 375 376 #[test] filter()377 fn filter() { 378 let (td, repo) = crate::test::repo_init(); 379 t!(File::create(&td.path().join("foo"))); 380 t!(File::create(&td.path().join("bar"))); 381 let mut opts = StatusOptions::new(); 382 opts.include_untracked(true).pathspec("foo"); 383 384 let statuses = t!(repo.statuses(Some(&mut opts))); 385 assert_eq!(statuses.iter().count(), 1); 386 let status = statuses.iter().next().unwrap(); 387 assert_eq!(status.path(), Some("foo")); 388 } 389 390 #[test] gitignore()391 fn gitignore() { 392 let (td, repo) = crate::test::repo_init(); 393 t!(t!(File::create(td.path().join(".gitignore"))).write_all(b"foo\n")); 394 assert!(!t!(repo.status_should_ignore(Path::new("bar")))); 395 assert!(t!(repo.status_should_ignore(Path::new("foo")))); 396 } 397 398 #[test] status_file()399 fn status_file() { 400 let (td, repo) = crate::test::repo_init(); 401 assert!(repo.status_file(Path::new("foo")).is_err()); 402 if cfg!(windows) { 403 assert!(repo.status_file(Path::new("bar\\foo.txt")).is_err()); 404 } 405 t!(File::create(td.path().join("foo"))); 406 if cfg!(windows) { 407 t!(::std::fs::create_dir_all(td.path().join("bar"))); 408 t!(File::create(td.path().join("bar").join("foo.txt"))); 409 } 410 let status = t!(repo.status_file(Path::new("foo"))); 411 assert!(status.contains(crate::Status::WT_NEW)); 412 if cfg!(windows) { 413 let status = t!(repo.status_file(Path::new("bar\\foo.txt"))); 414 assert!(status.contains(crate::Status::WT_NEW)); 415 } 416 } 417 } 418