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