git2/
status.rs

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
12/// Options that can be provided to `repo.statuses()` to control how the status
13/// information is gathered.
14pub struct StatusOptions {
15    raw: raw::git_status_options,
16    pathspec: Vec<CString>,
17    ptrs: Vec<*const c_char>,
18}
19
20/// Enumeration of possible methods of what can be shown through a status
21/// operation.
22#[derive(Copy, Clone)]
23pub enum StatusShow {
24    /// Only gives status based on HEAD to index comparison, not looking at
25    /// working directory changes.
26    Index,
27
28    /// Only gives status based on index to working directory comparison, not
29    /// comparing the index to the HEAD.
30    Workdir,
31
32    /// The default, this roughly matches `git status --porcelain` regarding
33    /// which files are included and in what order.
34    IndexAndWorkdir,
35}
36
37/// A container for a list of status information about a repository.
38///
39/// Each instance appears as if it were a collection, having a length and
40/// allowing indexing, as well as providing an iterator.
41pub struct Statuses<'repo> {
42    raw: *mut raw::git_status_list,
43
44    // Hm, not currently present, but can't hurt?
45    _marker: marker::PhantomData<&'repo Repository>,
46}
47
48/// An iterator over the statuses in a `Statuses` instance.
49pub struct StatusIter<'statuses> {
50    statuses: &'statuses Statuses<'statuses>,
51    range: Range<usize>,
52}
53
54/// A structure representing an entry in the `Statuses` structure.
55///
56/// Instances are created through the `.iter()` method or the `.get()` method.
57pub 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    /// Creates a new blank set of status options.
70    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    /// Select the files on which to report status.
84    ///
85    /// The default, if unspecified, is to show the index and the working
86    /// directory.
87    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.
101    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    /// Flag whether untracked files will be included.
118    ///
119    /// Untracked files will only be included if the workdir files are included
120    /// in the status "show" option.
121    pub fn include_untracked(&mut self, include: bool) -> &mut StatusOptions {
122        self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNTRACKED, include)
123    }
124
125    /// Flag whether ignored files will be included.
126    ///
127    /// The files will only be included if the workdir files are included
128    /// in the status "show" option.
129    pub fn include_ignored(&mut self, include: bool) -> &mut StatusOptions {
130        self.flag(raw::GIT_STATUS_OPT_INCLUDE_IGNORED, include)
131    }
132
133    /// Flag to include unmodified files.
134    pub fn include_unmodified(&mut self, include: bool) -> &mut StatusOptions {
135        self.flag(raw::GIT_STATUS_OPT_INCLUDE_UNMODIFIED, include)
136    }
137
138    /// Flag that submodules should be skipped.
139    ///
140    /// This only applies if there are no pending typechanges to the submodule
141    /// (either from or to another type).
142    pub fn exclude_submodules(&mut self, exclude: bool) -> &mut StatusOptions {
143        self.flag(raw::GIT_STATUS_OPT_EXCLUDE_SUBMODULES, exclude)
144    }
145
146    /// Flag that all files in untracked directories should be included.
147    ///
148    /// Normally if an entire directory is new then just the top-level directory
149    /// is included (with a trailing slash on the entry name).
150    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    /// Indicates that the given paths should be treated as literals paths, note
155    /// patterns.
156    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    /// Indicates that the contents of ignored directories should be included in
161    /// the status.
162    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    /// Indicates that rename detection should be processed between the head.
167    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    /// Indicates that rename detection should be run between the index and the
172    /// working directory.
173    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    /// Override the native case sensitivity for the file system and force the
178    /// output to be in case sensitive order.
179    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    /// Override the native case sensitivity for the file system and force the
184    /// output to be in case-insensitive order.
185    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    /// Indicates that rename detection should include rewritten files.
190    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    /// Bypasses the default status behavior of doing a "soft" index reload.
195    pub fn no_refresh(&mut self, include: bool) -> &mut StatusOptions {
196        self.flag(raw::GIT_STATUS_OPT_NO_REFRESH, include)
197    }
198
199    /// Refresh the stat cache in the index for files are unchanged but have
200    /// out of date stat information in the index.
201    ///
202    /// This will result in less work being done on subsequent calls to fetching
203    /// the status.
204    pub fn update_index(&mut self, include: bool) -> &mut StatusOptions {
205        self.flag(raw::GIT_STATUS_OPT_UPDATE_INDEX, include)
206    }
207
208    // erm...
209    #[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    // erm...
215    #[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    /// Set threshold above which similar files will be considered renames.
221    ///
222    /// This is equivalent to the `-M` option. Defaults to 50.
223    pub fn rename_threshold(&mut self, threshold: u16) -> &mut StatusOptions {
224        self.raw.rename_threshold = threshold;
225        self
226    }
227
228    /// Get a pointer to the inner list of status options.
229    ///
230    /// This function is unsafe as the returned structure has interior pointers
231    /// and may no longer be valid if these options continue to be mutated.
232    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    /// Gets a status entry from this list at the specified index.
241    ///
242    /// Returns `None` if the index is out of bounds.
243    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    /// Gets the count of status entries in this list.
251    ///
252    /// If there are no changes in status (according to the options given
253    /// when the status list was created), this should return 0.
254    pub fn len(&self) -> usize {
255        unsafe { raw::git_status_list_entrycount(self.raw) as usize }
256    }
257
258    /// Return `true` if there is no status entry in this list.
259    pub fn is_empty(&self) -> bool {
260        self.len() == 0
261    }
262
263    /// Returns an iterator over the statuses in this list.
264    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    /// Access the bytes for this entry's corresponding pathname
320    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    /// Access this entry's path name as a string.
332    ///
333    /// Returns `None` if the path is not valid utf-8.
334    pub fn path(&self) -> Option<&str> {
335        str::from_utf8(self.path_bytes()).ok()
336    }
337
338    /// Access the status flags for this file
339    pub fn status(&self) -> Status {
340        Status::from_bits_truncate(unsafe { (*self.raw).status as u32 })
341    }
342
343    /// Access detailed information about the differences between the file in
344    /// HEAD and the file in the index.
345    pub fn head_to_index(&self) -> Option<DiffDelta<'statuses>> {
346        unsafe { Binding::from_raw_opt((*self.raw).head_to_index) }
347    }
348
349    /// Access detailed information about the differences between the file in
350    /// the index and the file in the working directory.
351    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}