1 use std::ffi::CString; 2 use std::ops::Range; 3 use std::marker; 4 use std::mem; 5 use std::str; 6 use libc::{c_char, size_t, c_uint}; 7 8 use {raw, Status, DiffDelta, IntoCString, Repository}; 9 use util::Binding; 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, 73 raw::GIT_STATUS_OPTIONS_VERSION); 74 assert_eq!(r, 0); 75 StatusOptions { 76 raw: raw, 77 pathspec: Vec::new(), 78 ptrs: Vec::new(), 79 } 80 } 81 } 82 83 /// Select the files on which to report status. 84 /// 85 /// The default, if unspecified, is to show the index and the working 86 /// directory. show(&mut self, show: StatusShow) -> &mut StatusOptions87 pub fn show(&mut self, show: StatusShow) -> &mut StatusOptions { 88 self.raw.show = match show { 89 StatusShow::Index => raw::GIT_STATUS_SHOW_INDEX_ONLY, 90 StatusShow::Workdir => raw::GIT_STATUS_SHOW_WORKDIR_ONLY, 91 StatusShow::IndexAndWorkdir => raw::GIT_STATUS_SHOW_INDEX_AND_WORKDIR, 92 }; 93 self 94 } 95 96 /// Add a path pattern to match (using fnmatch-style matching). 97 /// 98 /// If the `disable_pathspec_match` option is given, then this is a literal 99 /// path to match. If this is not called, then there will be no patterns to 100 /// match and the entire directory will be used. pathspec<T: IntoCString>(&mut self, pathspec: T) -> &mut StatusOptions101 pub fn pathspec<T: IntoCString>(&mut self, pathspec: T) 102 -> &mut StatusOptions { 103 let s = pathspec.into_c_string().unwrap(); 104 self.ptrs.push(s.as_ptr()); 105 self.pathspec.push(s); 106 self 107 } 108 flag(&mut self, flag: raw::git_status_opt_t, val: bool) -> &mut StatusOptions109 fn flag(&mut self, flag: raw::git_status_opt_t, val: bool) 110 -> &mut StatusOptions { 111 if val { 112 self.raw.flags |= flag as c_uint; 113 } else { 114 self.raw.flags &= !(flag as c_uint); 115 } 116 self 117 } 118 119 /// Flag whether untracked files will be included. 120 /// 121 /// Untracked files will only be included if the workdir files are included 122 /// in the status "show" option. include_untracked(&mut self, include: bool) -> &mut StatusOptions123 pub fn include_untracked(&mut self, include: bool) -> &mut StatusOptions { 124 self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNTRACKED, include) 125 } 126 127 /// Flag whether ignored files will be included. 128 /// 129 /// The files will only be included if the workdir files are included 130 /// in the status "show" option. include_ignored(&mut self, include: bool) -> &mut StatusOptions131 pub fn include_ignored(&mut self, include: bool) -> &mut StatusOptions { 132 self.flag(raw::GIT_STATUS_OPT_INCLUDE_IGNORED, include) 133 } 134 135 /// Flag to include unmodified files. include_unmodified(&mut self, include: bool) -> &mut StatusOptions136 pub fn include_unmodified(&mut self, include: bool) -> &mut StatusOptions { 137 self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNMODIFIED, include) 138 } 139 140 /// Flag that submodules should be skipped. 141 /// 142 /// This only applies if there are no pending typechanges to the submodule 143 /// (either from or to another type). exclude_submodules(&mut self, exclude: bool) -> &mut StatusOptions144 pub fn exclude_submodules(&mut self, exclude: bool) -> &mut StatusOptions { 145 self.flag(raw::GIT_STATUS_OPT_EXCLUDE_SUBMODULES, exclude) 146 } 147 148 /// Flag that all files in untracked directories should be included. 149 /// 150 /// Normally if an entire directory is new then just the top-level directory 151 /// is included (with a trailing slash on the entry name). recurse_untracked_dirs(&mut self, include: bool) -> &mut StatusOptions152 pub fn recurse_untracked_dirs(&mut self, include: bool) 153 -> &mut StatusOptions { 154 self.flag(raw::GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS, include) 155 } 156 157 /// Indicates that the given paths should be treated as literals paths, note 158 /// patterns. disable_pathspec_match(&mut self, include: bool) -> &mut StatusOptions159 pub fn disable_pathspec_match(&mut self, include: bool) 160 -> &mut StatusOptions { 161 self.flag(raw::GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH, include) 162 } 163 164 /// Indicates that the contents of ignored directories should be included in 165 /// the status. recurse_ignored_dirs(&mut self, include: bool) -> &mut StatusOptions166 pub fn recurse_ignored_dirs(&mut self, include: bool) 167 -> &mut StatusOptions { 168 self.flag(raw::GIT_STATUS_OPT_RECURSE_IGNORED_DIRS, include) 169 } 170 171 /// Indicates that rename detection should be processed between the head. renames_head_to_index(&mut self, include: bool) -> &mut StatusOptions172 pub fn renames_head_to_index(&mut self, include: bool) 173 -> &mut StatusOptions { 174 self.flag(raw::GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX, include) 175 } 176 177 /// Indicates that rename detection should be run between the index and the 178 /// working directory. renames_index_to_workdir(&mut self, include: bool) -> &mut StatusOptions179 pub fn renames_index_to_workdir(&mut self, include: bool) 180 -> &mut StatusOptions { 181 self.flag(raw::GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR, include) 182 } 183 184 /// Override the native case sensitivity for the file system and force the 185 /// output to be in case sensitive order. sort_case_sensitively(&mut self, include: bool) -> &mut StatusOptions186 pub fn sort_case_sensitively(&mut self, include: bool) 187 -> &mut StatusOptions { 188 self.flag(raw::GIT_STATUS_OPT_SORT_CASE_SENSITIVELY, include) 189 } 190 191 /// Override the native case sensitivity for the file system and force the 192 /// output to be in case-insensitive order. sort_case_insensitively(&mut self, include: bool) -> &mut StatusOptions193 pub fn sort_case_insensitively(&mut self, include: bool) 194 -> &mut StatusOptions { 195 self.flag(raw::GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY, include) 196 } 197 198 /// Indicates that rename detection should include rewritten files. renames_from_rewrites(&mut self, include: bool) -> &mut StatusOptions199 pub fn renames_from_rewrites(&mut self, include: bool) 200 -> &mut StatusOptions { 201 self.flag(raw::GIT_STATUS_OPT_RENAMES_FROM_REWRITES, include) 202 } 203 204 /// Bypasses the default status behavior of doing a "soft" index reload. no_refresh(&mut self, include: bool) -> &mut StatusOptions205 pub fn no_refresh(&mut self, include: bool) -> &mut StatusOptions { 206 self.flag(raw::GIT_STATUS_OPT_NO_REFRESH, include) 207 } 208 209 /// Refresh the stat cache in the index for files are unchanged but have 210 /// out of date stat information in the index. 211 /// 212 /// This will result in less work being done on subsequent calls to fetching 213 /// the status. update_index(&mut self, include: bool) -> &mut StatusOptions214 pub fn update_index(&mut self, include: bool) -> &mut StatusOptions { 215 self.flag(raw::GIT_STATUS_OPT_UPDATE_INDEX, include) 216 } 217 218 // erm... 219 #[allow(missing_docs)] include_unreadable(&mut self, include: bool) -> &mut StatusOptions220 pub fn include_unreadable(&mut self, include: bool) -> &mut StatusOptions { 221 self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNREADABLE, include) 222 } 223 224 // erm... 225 #[allow(missing_docs)] include_unreadable_as_untracked(&mut self, include: bool) -> &mut StatusOptions226 pub fn include_unreadable_as_untracked(&mut self, include: bool) 227 -> &mut StatusOptions { 228 self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNREADABLE_AS_UNTRACKED, include) 229 } 230 231 /// Get a pointer to the inner list of status options. 232 /// 233 /// This function is unsafe as the returned structure has interior pointers 234 /// and may no longer be valid if these options continue to be mutated. raw(&mut self) -> *const raw::git_status_options235 pub unsafe fn raw(&mut self) -> *const raw::git_status_options { 236 self.raw.pathspec.strings = self.ptrs.as_ptr() as *mut _; 237 self.raw.pathspec.count = self.ptrs.len() as size_t; 238 &self.raw 239 } 240 } 241 242 impl<'repo> Statuses<'repo> { 243 /// Gets a status entry from this list at the specified index. 244 /// 245 /// Returns `None` if the index is out of bounds. get(&self, index: usize) -> Option<StatusEntry>246 pub fn get(&self, index: usize) -> Option<StatusEntry> { 247 unsafe { 248 let p = raw::git_status_byindex(self.raw, index as size_t); 249 Binding::from_raw_opt(p) 250 } 251 } 252 253 /// Gets the count of status entries in this list. 254 /// 255 /// If there are no changes in status (according to the options given 256 /// when the status list was created), this should return 0. len(&self) -> usize257 pub fn len(&self) -> usize { 258 unsafe { raw::git_status_list_entrycount(self.raw) as usize } 259 } 260 261 /// Return `true` if there is no status entry in this list. is_empty(&self) -> bool262 pub fn is_empty(&self) -> bool { 263 self.len() == 0 264 } 265 266 /// Returns an iterator over the statuses in this list. iter(&self) -> StatusIter267 pub fn iter(&self) -> StatusIter { 268 StatusIter { 269 statuses: self, 270 range: 0..self.len(), 271 } 272 } 273 } 274 275 impl<'repo> Binding for Statuses<'repo> { 276 type Raw = *mut raw::git_status_list; from_raw(raw: *mut raw::git_status_list) -> Statuses<'repo>277 unsafe fn from_raw(raw: *mut raw::git_status_list) -> Statuses<'repo> { 278 Statuses { raw: raw, _marker: marker::PhantomData } 279 } raw(&self) -> *mut raw::git_status_list280 fn raw(&self) -> *mut raw::git_status_list { self.raw } 281 } 282 283 impl<'repo> Drop for Statuses<'repo> { drop(&mut self)284 fn drop(&mut self) { 285 unsafe { raw::git_status_list_free(self.raw); } 286 } 287 } 288 289 impl<'a> Iterator for StatusIter<'a> { 290 type Item = StatusEntry<'a>; next(&mut self) -> Option<StatusEntry<'a>>291 fn next(&mut self) -> Option<StatusEntry<'a>> { 292 self.range.next().and_then(|i| self.statuses.get(i)) 293 } size_hint(&self) -> (usize, Option<usize>)294 fn size_hint(&self) -> (usize, Option<usize>) { self.range.size_hint() } 295 } 296 impl<'a> DoubleEndedIterator for StatusIter<'a> { next_back(&mut self) -> Option<StatusEntry<'a>>297 fn next_back(&mut self) -> Option<StatusEntry<'a>> { 298 self.range.next_back().and_then(|i| self.statuses.get(i)) 299 } 300 } 301 impl<'a> ExactSizeIterator for StatusIter<'a> {} 302 303 impl<'statuses> StatusEntry<'statuses> { 304 /// Access the bytes for this entry's corresponding pathname path_bytes(&self) -> &[u8]305 pub fn path_bytes(&self) -> &[u8] { 306 unsafe { 307 if (*self.raw).head_to_index.is_null() { 308 ::opt_bytes(self, (*(*self.raw).index_to_workdir).old_file.path) 309 } else { 310 ::opt_bytes(self, (*(*self.raw).head_to_index).old_file.path) 311 }.unwrap() 312 } 313 } 314 315 /// Access this entry's path name as a string. 316 /// 317 /// Returns `None` if the path is not valid utf-8. path(&self) -> Option<&str>318 pub fn path(&self) -> Option<&str> { str::from_utf8(self.path_bytes()).ok() } 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 { 329 Binding::from_raw_opt((*self.raw).head_to_index) 330 } 331 } 332 333 /// Access detailed information about the differences between the file in 334 /// the index and the file in the working directory. index_to_workdir(&self) -> Option<DiffDelta<'statuses>>335 pub fn index_to_workdir(&self) -> Option<DiffDelta<'statuses>> { 336 unsafe { 337 Binding::from_raw_opt((*self.raw).index_to_workdir) 338 } 339 } 340 } 341 342 impl<'statuses> Binding for StatusEntry<'statuses> { 343 type Raw = *const raw::git_status_entry; 344 from_raw(raw: *const raw::git_status_entry) -> StatusEntry<'statuses>345 unsafe fn from_raw(raw: *const raw::git_status_entry) 346 -> StatusEntry<'statuses> { 347 StatusEntry { raw: raw, _marker: marker::PhantomData } 348 } raw(&self) -> *const raw::git_status_entry349 fn raw(&self) -> *const raw::git_status_entry { self.raw } 350 } 351 352 #[cfg(test)] 353 mod tests { 354 use std::fs::File; 355 use std::path::Path; 356 use std::io::prelude::*; 357 use super::StatusOptions; 358 359 #[test] smoke()360 fn smoke() { 361 let (td, repo) = ::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(::Status::WT_NEW)); 369 assert!(!status.status().contains(::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) = ::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) 383 .pathspec("foo"); 384 385 let statuses = t!(repo.statuses(Some(&mut opts))); 386 assert_eq!(statuses.iter().count(), 1); 387 let status = statuses.iter().next().unwrap(); 388 assert_eq!(status.path(), Some("foo")); 389 } 390 391 #[test] gitignore()392 fn gitignore() { 393 let (td, repo) = ::test::repo_init(); 394 t!(t!(File::create(td.path().join(".gitignore"))).write_all(b"foo\n")); 395 assert!(!t!(repo.status_should_ignore(Path::new("bar")))); 396 assert!(t!(repo.status_should_ignore(Path::new("foo")))); 397 } 398 399 #[test] status_file()400 fn status_file() { 401 let (td, repo) = ::test::repo_init(); 402 assert!(repo.status_file(Path::new("foo")).is_err()); 403 if cfg!(windows) { 404 assert!(repo.status_file(Path::new("bar\\foo.txt")).is_err()); 405 } 406 t!(File::create(td.path().join("foo"))); 407 if cfg!(windows) { 408 t!(::std::fs::create_dir_all(td.path().join("bar"))); 409 t!(File::create(td.path().join("bar").join("foo.txt"))); 410 } 411 let status = t!(repo.status_file(Path::new("foo"))); 412 assert!(status.contains(::Status::WT_NEW)); 413 if cfg!(windows) { 414 let status = t!(repo.status_file(Path::new("bar\\foo.txt"))); 415 assert!(status.contains(::Status::WT_NEW)); 416 } 417 } 418 } 419