1use libc::{c_char, c_uint, size_t};
2use std::ffi::CString;
3use std::iter::FusedIterator;
4use std::marker;
5use std::mem;
6use std::ops::Range;
7use std::str;
8
9use crate::util::{self, Binding};
10use crate::{raw, DiffDelta, IntoCString, Repository, Status};
11
12pub struct StatusOptions {
15    raw: raw::git_status_options,
16    pathspec: Vec<CString>,
17    ptrs: Vec<*const c_char>,
18}
19
20#[derive(Copy, Clone)]
23pub enum StatusShow {
24    Index,
27
28    Workdir,
31
32    IndexAndWorkdir,
35}
36
37pub struct Statuses<'repo> {
42    raw: *mut raw::git_status_list,
43
44    _marker: marker::PhantomData<&'repo Repository>,
46}
47
48pub struct StatusIter<'statuses> {
50    statuses: &'statuses Statuses<'statuses>,
51    range: Range<usize>,
52}
53
54pub struct StatusEntry<'statuses> {
58    raw: *const raw::git_status_entry,
59    _marker: marker::PhantomData<&'statuses DiffDelta<'statuses>>,
60}
61
62impl Default for StatusOptions {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68impl StatusOptions {
69    pub fn new() -> StatusOptions {
71        unsafe {
72            let mut raw = mem::zeroed();
73            let r = raw::git_status_init_options(&mut raw, raw::GIT_STATUS_OPTIONS_VERSION);
74            assert_eq!(r, 0);
75            StatusOptions {
76                raw,
77                pathspec: Vec::new(),
78                ptrs: Vec::new(),
79            }
80        }
81    }
82
83    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    pub fn pathspec<T: IntoCString>(&mut self, pathspec: T) -> &mut StatusOptions {
102        let s = util::cstring_to_repo_path(pathspec).unwrap();
103        self.ptrs.push(s.as_ptr());
104        self.pathspec.push(s);
105        self
106    }
107
108    fn flag(&mut self, flag: raw::git_status_opt_t, val: bool) -> &mut StatusOptions {
109        if val {
110            self.raw.flags |= flag as c_uint;
111        } else {
112            self.raw.flags &= !(flag as c_uint);
113        }
114        self
115    }
116
117    pub fn include_untracked(&mut self, include: bool) -> &mut StatusOptions {
122        self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNTRACKED, include)
123    }
124
125    pub fn include_ignored(&mut self, include: bool) -> &mut StatusOptions {
130        self.flag(raw::GIT_STATUS_OPT_INCLUDE_IGNORED, include)
131    }
132
133    pub fn include_unmodified(&mut self, include: bool) -> &mut StatusOptions {
135        self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNMODIFIED, include)
136    }
137
138    pub fn exclude_submodules(&mut self, exclude: bool) -> &mut StatusOptions {
143        self.flag(raw::GIT_STATUS_OPT_EXCLUDE_SUBMODULES, exclude)
144    }
145
146    pub fn recurse_untracked_dirs(&mut self, include: bool) -> &mut StatusOptions {
151        self.flag(raw::GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS, include)
152    }
153
154    pub fn disable_pathspec_match(&mut self, include: bool) -> &mut StatusOptions {
157        self.flag(raw::GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH, include)
158    }
159
160    pub fn recurse_ignored_dirs(&mut self, include: bool) -> &mut StatusOptions {
163        self.flag(raw::GIT_STATUS_OPT_RECURSE_IGNORED_DIRS, include)
164    }
165
166    pub fn renames_head_to_index(&mut self, include: bool) -> &mut StatusOptions {
168        self.flag(raw::GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX, include)
169    }
170
171    pub fn renames_index_to_workdir(&mut self, include: bool) -> &mut StatusOptions {
174        self.flag(raw::GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR, include)
175    }
176
177    pub fn sort_case_sensitively(&mut self, include: bool) -> &mut StatusOptions {
180        self.flag(raw::GIT_STATUS_OPT_SORT_CASE_SENSITIVELY, include)
181    }
182
183    pub fn sort_case_insensitively(&mut self, include: bool) -> &mut StatusOptions {
186        self.flag(raw::GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY, include)
187    }
188
189    pub fn renames_from_rewrites(&mut self, include: bool) -> &mut StatusOptions {
191        self.flag(raw::GIT_STATUS_OPT_RENAMES_FROM_REWRITES, include)
192    }
193
194    pub fn no_refresh(&mut self, include: bool) -> &mut StatusOptions {
196        self.flag(raw::GIT_STATUS_OPT_NO_REFRESH, include)
197    }
198
199    pub fn update_index(&mut self, include: bool) -> &mut StatusOptions {
205        self.flag(raw::GIT_STATUS_OPT_UPDATE_INDEX, include)
206    }
207
208    #[allow(missing_docs)]
210    pub fn include_unreadable(&mut self, include: bool) -> &mut StatusOptions {
211        self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNREADABLE, include)
212    }
213
214    #[allow(missing_docs)]
216    pub fn include_unreadable_as_untracked(&mut self, include: bool) -> &mut StatusOptions {
217        self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNREADABLE_AS_UNTRACKED, include)
218    }
219
220    pub fn rename_threshold(&mut self, threshold: u16) -> &mut StatusOptions {
224        self.raw.rename_threshold = threshold;
225        self
226    }
227
228    pub unsafe fn raw(&mut self) -> *const raw::git_status_options {
233        self.raw.pathspec.strings = self.ptrs.as_ptr() as *mut _;
234        self.raw.pathspec.count = self.ptrs.len() as size_t;
235        &self.raw
236    }
237}
238
239impl<'repo> Statuses<'repo> {
240    pub fn get(&self, index: usize) -> Option<StatusEntry<'_>> {
244        unsafe {
245            let p = raw::git_status_byindex(self.raw, index as size_t);
246            Binding::from_raw_opt(p)
247        }
248    }
249
250    pub fn len(&self) -> usize {
255        unsafe { raw::git_status_list_entrycount(self.raw) as usize }
256    }
257
258    pub fn is_empty(&self) -> bool {
260        self.len() == 0
261    }
262
263    pub fn iter(&self) -> StatusIter<'_> {
265        StatusIter {
266            statuses: self,
267            range: 0..self.len(),
268        }
269    }
270}
271
272impl<'repo> Binding for Statuses<'repo> {
273    type Raw = *mut raw::git_status_list;
274    unsafe fn from_raw(raw: *mut raw::git_status_list) -> Statuses<'repo> {
275        Statuses {
276            raw,
277            _marker: marker::PhantomData,
278        }
279    }
280    fn raw(&self) -> *mut raw::git_status_list {
281        self.raw
282    }
283}
284
285impl<'repo> Drop for Statuses<'repo> {
286    fn drop(&mut self) {
287        unsafe {
288            raw::git_status_list_free(self.raw);
289        }
290    }
291}
292
293impl<'a> Iterator for StatusIter<'a> {
294    type Item = StatusEntry<'a>;
295    fn next(&mut self) -> Option<StatusEntry<'a>> {
296        self.range.next().and_then(|i| self.statuses.get(i))
297    }
298    fn size_hint(&self) -> (usize, Option<usize>) {
299        self.range.size_hint()
300    }
301}
302impl<'a> DoubleEndedIterator for StatusIter<'a> {
303    fn next_back(&mut self) -> Option<StatusEntry<'a>> {
304        self.range.next_back().and_then(|i| self.statuses.get(i))
305    }
306}
307impl<'a> FusedIterator for StatusIter<'a> {}
308impl<'a> ExactSizeIterator for StatusIter<'a> {}
309
310impl<'a> IntoIterator for &'a Statuses<'a> {
311    type Item = StatusEntry<'a>;
312    type IntoIter = StatusIter<'a>;
313    fn into_iter(self) -> Self::IntoIter {
314        self.iter()
315    }
316}
317
318impl<'statuses> StatusEntry<'statuses> {
319    pub fn path_bytes(&self) -> &[u8] {
321        unsafe {
322            if (*self.raw).head_to_index.is_null() {
323                crate::opt_bytes(self, (*(*self.raw).index_to_workdir).old_file.path)
324            } else {
325                crate::opt_bytes(self, (*(*self.raw).head_to_index).old_file.path)
326            }
327            .unwrap()
328        }
329    }
330
331    pub fn path(&self) -> Option<&str> {
335        str::from_utf8(self.path_bytes()).ok()
336    }
337
338    pub fn status(&self) -> Status {
340        Status::from_bits_truncate(unsafe { (*self.raw).status as u32 })
341    }
342
343    pub fn head_to_index(&self) -> Option<DiffDelta<'statuses>> {
346        unsafe { Binding::from_raw_opt((*self.raw).head_to_index) }
347    }
348
349    pub fn index_to_workdir(&self) -> Option<DiffDelta<'statuses>> {
352        unsafe { Binding::from_raw_opt((*self.raw).index_to_workdir) }
353    }
354}
355
356impl<'statuses> Binding for StatusEntry<'statuses> {
357    type Raw = *const raw::git_status_entry;
358
359    unsafe fn from_raw(raw: *const raw::git_status_entry) -> StatusEntry<'statuses> {
360        StatusEntry {
361            raw,
362            _marker: marker::PhantomData,
363        }
364    }
365    fn raw(&self) -> *const raw::git_status_entry {
366        self.raw
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use super::StatusOptions;
373    use std::fs::File;
374    use std::io::prelude::*;
375    use std::path::Path;
376
377    #[test]
378    fn smoke() {
379        let (td, repo) = crate::test::repo_init();
380        assert_eq!(repo.statuses(None).unwrap().len(), 0);
381        File::create(&td.path().join("foo")).unwrap();
382        let statuses = repo.statuses(None).unwrap();
383        assert_eq!(statuses.iter().count(), 1);
384        let status = statuses.iter().next().unwrap();
385        assert_eq!(status.path(), Some("foo"));
386        assert!(status.status().contains(crate::Status::WT_NEW));
387        assert!(!status.status().contains(crate::Status::INDEX_NEW));
388        assert!(status.head_to_index().is_none());
389        let diff = status.index_to_workdir().unwrap();
390        assert_eq!(diff.old_file().path_bytes().unwrap(), b"foo");
391        assert_eq!(diff.new_file().path_bytes().unwrap(), b"foo");
392    }
393
394    #[test]
395    fn filter() {
396        let (td, repo) = crate::test::repo_init();
397        t!(File::create(&td.path().join("foo")));
398        t!(File::create(&td.path().join("bar")));
399        let mut opts = StatusOptions::new();
400        opts.include_untracked(true).pathspec("foo");
401
402        let statuses = t!(repo.statuses(Some(&mut opts)));
403        assert_eq!(statuses.iter().count(), 1);
404        let status = statuses.iter().next().unwrap();
405        assert_eq!(status.path(), Some("foo"));
406    }
407
408    #[test]
409    fn gitignore() {
410        let (td, repo) = crate::test::repo_init();
411        t!(t!(File::create(td.path().join(".gitignore"))).write_all(b"foo\n"));
412        assert!(!t!(repo.status_should_ignore(Path::new("bar"))));
413        assert!(t!(repo.status_should_ignore(Path::new("foo"))));
414    }
415
416    #[test]
417    fn status_file() {
418        let (td, repo) = crate::test::repo_init();
419        assert!(repo.status_file(Path::new("foo")).is_err());
420        if cfg!(windows) {
421            assert!(repo.status_file(Path::new("bar\\foo.txt")).is_err());
422        }
423        t!(File::create(td.path().join("foo")));
424        if cfg!(windows) {
425            t!(::std::fs::create_dir_all(td.path().join("bar")));
426            t!(File::create(td.path().join("bar").join("foo.txt")));
427        }
428        let status = t!(repo.status_file(Path::new("foo")));
429        assert!(status.contains(crate::Status::WT_NEW));
430        if cfg!(windows) {
431            let status = t!(repo.status_file(Path::new("bar\\foo.txt")));
432            assert!(status.contains(crate::Status::WT_NEW));
433        }
434    }
435}