]> git.proxmox.com Git - rustc.git/blame - vendor/git2/src/status.rs
New upstream version 1.74.1+dfsg1
[rustc.git] / vendor / git2 / src / status.rs
CommitLineData
0a29b90c
FG
1use libc::{c_char, c_uint, size_t};
2use std::ffi::CString;
781aab86 3use std::iter::FusedIterator;
0a29b90c
FG
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}
781aab86 307impl<'a> FusedIterator for StatusIter<'a> {}
0a29b90c
FG
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}