| use libc::{c_char, c_uint, size_t}; |
| use std::ffi::CString; |
| use std::marker; |
| use std::mem; |
| use std::ops::Range; |
| use std::str; |
| |
| use crate::util::{self, Binding}; |
| use crate::{raw, DiffDelta, IntoCString, Repository, Status}; |
| |
| /// Options that can be provided to `repo.statuses()` to control how the status |
| /// information is gathered. |
| pub struct StatusOptions { |
| raw: raw::git_status_options, |
| pathspec: Vec<CString>, |
| ptrs: Vec<*const c_char>, |
| } |
| |
| /// Enumeration of possible methods of what can be shown through a status |
| /// operation. |
| #[derive(Copy, Clone)] |
| pub enum StatusShow { |
| /// Only gives status based on HEAD to index comparison, not looking at |
| /// working directory changes. |
| Index, |
| |
| /// Only gives status based on index to working directory comparison, not |
| /// comparing the index to the HEAD. |
| Workdir, |
| |
| /// The default, this roughly matches `git status --porcelain` regarding |
| /// which files are included and in what order. |
| IndexAndWorkdir, |
| } |
| |
| /// A container for a list of status information about a repository. |
| /// |
| /// Each instance appears as if it were a collection, having a length and |
| /// allowing indexing, as well as providing an iterator. |
| pub struct Statuses<'repo> { |
| raw: *mut raw::git_status_list, |
| |
| // Hm, not currently present, but can't hurt? |
| _marker: marker::PhantomData<&'repo Repository>, |
| } |
| |
| /// An iterator over the statuses in a `Statuses` instance. |
| pub struct StatusIter<'statuses> { |
| statuses: &'statuses Statuses<'statuses>, |
| range: Range<usize>, |
| } |
| |
| /// A structure representing an entry in the `Statuses` structure. |
| /// |
| /// Instances are created through the `.iter()` method or the `.get()` method. |
| pub struct StatusEntry<'statuses> { |
| raw: *const raw::git_status_entry, |
| _marker: marker::PhantomData<&'statuses DiffDelta<'statuses>>, |
| } |
| |
| impl Default for StatusOptions { |
| fn default() -> Self { |
| Self::new() |
| } |
| } |
| |
| impl StatusOptions { |
| /// Creates a new blank set of status options. |
| pub fn new() -> StatusOptions { |
| unsafe { |
| let mut raw = mem::zeroed(); |
| let r = raw::git_status_init_options(&mut raw, raw::GIT_STATUS_OPTIONS_VERSION); |
| assert_eq!(r, 0); |
| StatusOptions { |
| raw, |
| pathspec: Vec::new(), |
| ptrs: Vec::new(), |
| } |
| } |
| } |
| |
| /// Select the files on which to report status. |
| /// |
| /// The default, if unspecified, is to show the index and the working |
| /// directory. |
| pub fn show(&mut self, show: StatusShow) -> &mut StatusOptions { |
| self.raw.show = match show { |
| StatusShow::Index => raw::GIT_STATUS_SHOW_INDEX_ONLY, |
| StatusShow::Workdir => raw::GIT_STATUS_SHOW_WORKDIR_ONLY, |
| StatusShow::IndexAndWorkdir => raw::GIT_STATUS_SHOW_INDEX_AND_WORKDIR, |
| }; |
| self |
| } |
| |
| /// Add a path pattern to match (using fnmatch-style matching). |
| /// |
| /// If the `disable_pathspec_match` option is given, then this is a literal |
| /// path to match. If this is not called, then there will be no patterns to |
| /// match and the entire directory will be used. |
| pub fn pathspec<T: IntoCString>(&mut self, pathspec: T) -> &mut StatusOptions { |
| let s = util::cstring_to_repo_path(pathspec).unwrap(); |
| self.ptrs.push(s.as_ptr()); |
| self.pathspec.push(s); |
| self |
| } |
| |
| fn flag(&mut self, flag: raw::git_status_opt_t, val: bool) -> &mut StatusOptions { |
| if val { |
| self.raw.flags |= flag as c_uint; |
| } else { |
| self.raw.flags &= !(flag as c_uint); |
| } |
| self |
| } |
| |
| /// Flag whether untracked files will be included. |
| /// |
| /// Untracked files will only be included if the workdir files are included |
| /// in the status "show" option. |
| pub fn include_untracked(&mut self, include: bool) -> &mut StatusOptions { |
| self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNTRACKED, include) |
| } |
| |
| /// Flag whether ignored files will be included. |
| /// |
| /// The files will only be included if the workdir files are included |
| /// in the status "show" option. |
| pub fn include_ignored(&mut self, include: bool) -> &mut StatusOptions { |
| self.flag(raw::GIT_STATUS_OPT_INCLUDE_IGNORED, include) |
| } |
| |
| /// Flag to include unmodified files. |
| pub fn include_unmodified(&mut self, include: bool) -> &mut StatusOptions { |
| self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNMODIFIED, include) |
| } |
| |
| /// Flag that submodules should be skipped. |
| /// |
| /// This only applies if there are no pending typechanges to the submodule |
| /// (either from or to another type). |
| pub fn exclude_submodules(&mut self, exclude: bool) -> &mut StatusOptions { |
| self.flag(raw::GIT_STATUS_OPT_EXCLUDE_SUBMODULES, exclude) |
| } |
| |
| /// Flag that all files in untracked directories should be included. |
| /// |
| /// Normally if an entire directory is new then just the top-level directory |
| /// is included (with a trailing slash on the entry name). |
| pub fn recurse_untracked_dirs(&mut self, include: bool) -> &mut StatusOptions { |
| self.flag(raw::GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS, include) |
| } |
| |
| /// Indicates that the given paths should be treated as literals paths, note |
| /// patterns. |
| pub fn disable_pathspec_match(&mut self, include: bool) -> &mut StatusOptions { |
| self.flag(raw::GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH, include) |
| } |
| |
| /// Indicates that the contents of ignored directories should be included in |
| /// the status. |
| pub fn recurse_ignored_dirs(&mut self, include: bool) -> &mut StatusOptions { |
| self.flag(raw::GIT_STATUS_OPT_RECURSE_IGNORED_DIRS, include) |
| } |
| |
| /// Indicates that rename detection should be processed between the head. |
| pub fn renames_head_to_index(&mut self, include: bool) -> &mut StatusOptions { |
| self.flag(raw::GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX, include) |
| } |
| |
| /// Indicates that rename detection should be run between the index and the |
| /// working directory. |
| pub fn renames_index_to_workdir(&mut self, include: bool) -> &mut StatusOptions { |
| self.flag(raw::GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR, include) |
| } |
| |
| /// Override the native case sensitivity for the file system and force the |
| /// output to be in case sensitive order. |
| pub fn sort_case_sensitively(&mut self, include: bool) -> &mut StatusOptions { |
| self.flag(raw::GIT_STATUS_OPT_SORT_CASE_SENSITIVELY, include) |
| } |
| |
| /// Override the native case sensitivity for the file system and force the |
| /// output to be in case-insensitive order. |
| pub fn sort_case_insensitively(&mut self, include: bool) -> &mut StatusOptions { |
| self.flag(raw::GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY, include) |
| } |
| |
| /// Indicates that rename detection should include rewritten files. |
| pub fn renames_from_rewrites(&mut self, include: bool) -> &mut StatusOptions { |
| self.flag(raw::GIT_STATUS_OPT_RENAMES_FROM_REWRITES, include) |
| } |
| |
| /// Bypasses the default status behavior of doing a "soft" index reload. |
| pub fn no_refresh(&mut self, include: bool) -> &mut StatusOptions { |
| self.flag(raw::GIT_STATUS_OPT_NO_REFRESH, include) |
| } |
| |
| /// Refresh the stat cache in the index for files are unchanged but have |
| /// out of date stat information in the index. |
| /// |
| /// This will result in less work being done on subsequent calls to fetching |
| /// the status. |
| pub fn update_index(&mut self, include: bool) -> &mut StatusOptions { |
| self.flag(raw::GIT_STATUS_OPT_UPDATE_INDEX, include) |
| } |
| |
| // erm... |
| #[allow(missing_docs)] |
| pub fn include_unreadable(&mut self, include: bool) -> &mut StatusOptions { |
| self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNREADABLE, include) |
| } |
| |
| // erm... |
| #[allow(missing_docs)] |
| pub fn include_unreadable_as_untracked(&mut self, include: bool) -> &mut StatusOptions { |
| self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNREADABLE_AS_UNTRACKED, include) |
| } |
| |
| /// Get a pointer to the inner list of status options. |
| /// |
| /// This function is unsafe as the returned structure has interior pointers |
| /// and may no longer be valid if these options continue to be mutated. |
| pub unsafe fn raw(&mut self) -> *const raw::git_status_options { |
| self.raw.pathspec.strings = self.ptrs.as_ptr() as *mut _; |
| self.raw.pathspec.count = self.ptrs.len() as size_t; |
| &self.raw |
| } |
| } |
| |
| impl<'repo> Statuses<'repo> { |
| /// Gets a status entry from this list at the specified index. |
| /// |
| /// Returns `None` if the index is out of bounds. |
| pub fn get(&self, index: usize) -> Option<StatusEntry<'_>> { |
| unsafe { |
| let p = raw::git_status_byindex(self.raw, index as size_t); |
| Binding::from_raw_opt(p) |
| } |
| } |
| |
| /// Gets the count of status entries in this list. |
| /// |
| /// If there are no changes in status (according to the options given |
| /// when the status list was created), this should return 0. |
| pub fn len(&self) -> usize { |
| unsafe { raw::git_status_list_entrycount(self.raw) as usize } |
| } |
| |
| /// Return `true` if there is no status entry in this list. |
| pub fn is_empty(&self) -> bool { |
| self.len() == 0 |
| } |
| |
| /// Returns an iterator over the statuses in this list. |
| pub fn iter(&self) -> StatusIter<'_> { |
| StatusIter { |
| statuses: self, |
| range: 0..self.len(), |
| } |
| } |
| } |
| |
| impl<'repo> Binding for Statuses<'repo> { |
| type Raw = *mut raw::git_status_list; |
| unsafe fn from_raw(raw: *mut raw::git_status_list) -> Statuses<'repo> { |
| Statuses { |
| raw, |
| _marker: marker::PhantomData, |
| } |
| } |
| fn raw(&self) -> *mut raw::git_status_list { |
| self.raw |
| } |
| } |
| |
| impl<'repo> Drop for Statuses<'repo> { |
| fn drop(&mut self) { |
| unsafe { |
| raw::git_status_list_free(self.raw); |
| } |
| } |
| } |
| |
| impl<'a> Iterator for StatusIter<'a> { |
| type Item = StatusEntry<'a>; |
| fn next(&mut self) -> Option<StatusEntry<'a>> { |
| self.range.next().and_then(|i| self.statuses.get(i)) |
| } |
| fn size_hint(&self) -> (usize, Option<usize>) { |
| self.range.size_hint() |
| } |
| } |
| impl<'a> DoubleEndedIterator for StatusIter<'a> { |
| fn next_back(&mut self) -> Option<StatusEntry<'a>> { |
| self.range.next_back().and_then(|i| self.statuses.get(i)) |
| } |
| } |
| impl<'a> ExactSizeIterator for StatusIter<'a> {} |
| |
| impl<'statuses> StatusEntry<'statuses> { |
| /// Access the bytes for this entry's corresponding pathname |
| pub fn path_bytes(&self) -> &[u8] { |
| unsafe { |
| if (*self.raw).head_to_index.is_null() { |
| crate::opt_bytes(self, (*(*self.raw).index_to_workdir).old_file.path) |
| } else { |
| crate::opt_bytes(self, (*(*self.raw).head_to_index).old_file.path) |
| } |
| .unwrap() |
| } |
| } |
| |
| /// Access this entry's path name as a string. |
| /// |
| /// Returns `None` if the path is not valid utf-8. |
| pub fn path(&self) -> Option<&str> { |
| str::from_utf8(self.path_bytes()).ok() |
| } |
| |
| /// Access the status flags for this file |
| pub fn status(&self) -> Status { |
| Status::from_bits_truncate(unsafe { (*self.raw).status as u32 }) |
| } |
| |
| /// Access detailed information about the differences between the file in |
| /// HEAD and the file in the index. |
| pub fn head_to_index(&self) -> Option<DiffDelta<'statuses>> { |
| unsafe { Binding::from_raw_opt((*self.raw).head_to_index) } |
| } |
| |
| /// Access detailed information about the differences between the file in |
| /// the index and the file in the working directory. |
| pub fn index_to_workdir(&self) -> Option<DiffDelta<'statuses>> { |
| unsafe { Binding::from_raw_opt((*self.raw).index_to_workdir) } |
| } |
| } |
| |
| impl<'statuses> Binding for StatusEntry<'statuses> { |
| type Raw = *const raw::git_status_entry; |
| |
| unsafe fn from_raw(raw: *const raw::git_status_entry) -> StatusEntry<'statuses> { |
| StatusEntry { |
| raw, |
| _marker: marker::PhantomData, |
| } |
| } |
| fn raw(&self) -> *const raw::git_status_entry { |
| self.raw |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::StatusOptions; |
| use std::fs::File; |
| use std::io::prelude::*; |
| use std::path::Path; |
| |
| #[test] |
| fn smoke() { |
| let (td, repo) = crate::test::repo_init(); |
| assert_eq!(repo.statuses(None).unwrap().len(), 0); |
| File::create(&td.path().join("foo")).unwrap(); |
| let statuses = repo.statuses(None).unwrap(); |
| assert_eq!(statuses.iter().count(), 1); |
| let status = statuses.iter().next().unwrap(); |
| assert_eq!(status.path(), Some("foo")); |
| assert!(status.status().contains(crate::Status::WT_NEW)); |
| assert!(!status.status().contains(crate::Status::INDEX_NEW)); |
| assert!(status.head_to_index().is_none()); |
| let diff = status.index_to_workdir().unwrap(); |
| assert_eq!(diff.old_file().path_bytes().unwrap(), b"foo"); |
| assert_eq!(diff.new_file().path_bytes().unwrap(), b"foo"); |
| } |
| |
| #[test] |
| fn filter() { |
| let (td, repo) = crate::test::repo_init(); |
| t!(File::create(&td.path().join("foo"))); |
| t!(File::create(&td.path().join("bar"))); |
| let mut opts = StatusOptions::new(); |
| opts.include_untracked(true).pathspec("foo"); |
| |
| let statuses = t!(repo.statuses(Some(&mut opts))); |
| assert_eq!(statuses.iter().count(), 1); |
| let status = statuses.iter().next().unwrap(); |
| assert_eq!(status.path(), Some("foo")); |
| } |
| |
| #[test] |
| fn gitignore() { |
| let (td, repo) = crate::test::repo_init(); |
| t!(t!(File::create(td.path().join(".gitignore"))).write_all(b"foo\n")); |
| assert!(!t!(repo.status_should_ignore(Path::new("bar")))); |
| assert!(t!(repo.status_should_ignore(Path::new("foo")))); |
| } |
| |
| #[test] |
| fn status_file() { |
| let (td, repo) = crate::test::repo_init(); |
| assert!(repo.status_file(Path::new("foo")).is_err()); |
| if cfg!(windows) { |
| assert!(repo.status_file(Path::new("bar\\foo.txt")).is_err()); |
| } |
| t!(File::create(td.path().join("foo"))); |
| if cfg!(windows) { |
| t!(::std::fs::create_dir_all(td.path().join("bar"))); |
| t!(File::create(td.path().join("bar").join("foo.txt"))); |
| } |
| let status = t!(repo.status_file(Path::new("foo"))); |
| assert!(status.contains(crate::Status::WT_NEW)); |
| if cfg!(windows) { |
| let status = t!(repo.status_file(Path::new("bar\\foo.txt"))); |
| assert!(status.contains(crate::Status::WT_NEW)); |
| } |
| } |
| } |