Skip to main content

gix_index/access/
mod.rs

1use std::{cmp::Ordering, ops::Range};
2
3use bstr::{BStr, ByteSlice, ByteVec};
4use filetime::FileTime;
5
6use crate::{
7    entry,
8    entry::{Stage, StageRaw},
9    extension, AccelerateLookup, Entry, PathStorage, PathStorageRef, State, Version,
10};
11
12// TODO: integrate this somehow, somewhere, depending on later usage.
13#[allow(dead_code)]
14mod sparse;
15
16/// General information and entries
17impl State {
18    /// Return the version used to store this state's information on disk.
19    pub fn version(&self) -> Version {
20        self.version
21    }
22
23    /// Returns time at which the state was created, indicating its freshness compared to other files on disk.
24    pub fn timestamp(&self) -> FileTime {
25        self.timestamp
26    }
27
28    /// Updates the timestamp of this state, indicating its freshness compared to other files on disk.
29    ///
30    /// Be careful about using this as setting a timestamp without correctly updating the index
31    /// **will cause (file system) race conditions** see racy-git.txt in the git documentation
32    /// for more details.
33    pub fn set_timestamp(&mut self, timestamp: FileTime) {
34        self.timestamp = timestamp;
35    }
36
37    /// Return the kind of hashes used in this instance.
38    pub fn object_hash(&self) -> gix_hash::Kind {
39        self.object_hash
40    }
41
42    /// Return our entries
43    pub fn entries(&self) -> &[Entry] {
44        &self.entries
45    }
46    /// Return our path backing, the place which keeps all paths one after another, with entries storing only the range to access them.
47    pub fn path_backing(&self) -> &PathStorageRef {
48        &self.path_backing
49    }
50
51    /// Runs `filter_map` on all entries, returning an iterator over all paths along with the result of `filter_map`.
52    pub fn entries_with_paths_by_filter_map<'a, T>(
53        &'a self,
54        mut filter_map: impl FnMut(&'a BStr, &Entry) -> Option<T> + 'a,
55    ) -> impl Iterator<Item = (&'a BStr, T)> + 'a {
56        self.entries.iter().filter_map(move |e| {
57            let p = e.path(self);
58            filter_map(p, e).map(|t| (p, t))
59        })
60    }
61    /// Return mutable entries along with their path, as obtained from `backing`.
62    pub fn entries_mut_with_paths_in<'state, 'backing>(
63        &'state mut self,
64        backing: &'backing PathStorageRef,
65    ) -> impl Iterator<Item = (&'state mut Entry, &'backing BStr)> {
66        self.entries.iter_mut().map(move |e| {
67            let path = backing[e.path.clone()].as_bstr();
68            (e, path)
69        })
70    }
71
72    /// Find the entry index in [`entries()`][State::entries()] matching the given repository-relative
73    /// `path` and `stage`, or `None`.
74    ///
75    /// Use the index for accessing multiple stages if they exists, but at least the single matching entry.
76    pub fn entry_index_by_path_and_stage(&self, path: &BStr, stage: entry::Stage) -> Option<usize> {
77        let mut stage_cmp = Ordering::Equal;
78        let idx = self
79            .entries
80            .binary_search_by(|e| {
81                let res = e.path(self).cmp(path);
82                if res.is_eq() {
83                    stage_cmp = e.stage().cmp(&stage);
84                }
85                res
86            })
87            .ok()?;
88        self.entry_index_by_idx_and_stage(path, idx, stage as StageRaw, stage_cmp)
89    }
90
91    /// Walk as far in `direction` as possible, with [`Ordering::Greater`] towards higher stages, and [`Ordering::Less`]
92    /// towards lower stages, and return the lowest or highest seen stage.
93    /// Return `None` if there is no greater or smaller stage.
94    fn walk_entry_stages(&self, path: &BStr, base: usize, direction: Ordering) -> Option<usize> {
95        match direction {
96            Ordering::Greater => self
97                .entries
98                .get(base + 1..)?
99                .iter()
100                .enumerate()
101                .take_while(|(_, e)| e.path(self) == path)
102                .last()
103                .map(|(idx, _)| base + 1 + idx),
104            Ordering::Equal => Some(base),
105            Ordering::Less => self.entries[..base]
106                .iter()
107                .enumerate()
108                .rev()
109                .take_while(|(_, e)| e.path(self) == path)
110                .last()
111                .map(|(idx, _)| idx),
112        }
113    }
114
115    fn entry_index_by_idx_and_stage(
116        &self,
117        path: &BStr,
118        idx: usize,
119        wanted_stage: entry::StageRaw,
120        stage_cmp: Ordering,
121    ) -> Option<usize> {
122        match stage_cmp {
123            Ordering::Greater => self.entries[..idx]
124                .iter()
125                .enumerate()
126                .rev()
127                .take_while(|(_, e)| e.path(self) == path)
128                .find_map(|(idx, e)| (e.stage_raw() == wanted_stage).then_some(idx)),
129            Ordering::Equal => Some(idx),
130            Ordering::Less => self
131                .entries
132                .get(idx + 1..)?
133                .iter()
134                .enumerate()
135                .take_while(|(_, e)| e.path(self) == path)
136                .find_map(|(ofs, e)| (e.stage_raw() == wanted_stage).then_some(idx + ofs + 1)),
137        }
138    }
139
140    /// Return a data structure to help with case-insensitive lookups.
141    ///
142    /// It's required perform any case-insensitive lookup.
143    /// TODO: needs multi-threaded insertion, raw-table to have multiple locks depending on bucket.
144    pub fn prepare_icase_backing(&self) -> AccelerateLookup<'_> {
145        let _span = gix_features::trace::detail!("prepare_icase_backing", entries = self.entries.len());
146        let mut out = AccelerateLookup::with_capacity(self.entries.len());
147        for entry in &self.entries {
148            let entry_path = entry.path(self);
149            let hash = AccelerateLookup::icase_hash(entry_path);
150            out.icase_entries
151                .insert_unique(hash, entry, |e| AccelerateLookup::icase_hash(e.path(self)));
152            if entry_is_dir(entry) {
153                out.icase_dirs.insert_unique(
154                    hash,
155                    crate::DirEntry {
156                        entry,
157                        dir_end: entry.path.end,
158                    },
159                    |dir| AccelerateLookup::icase_hash(dir.path(self)),
160                );
161            }
162
163            let mut last_pos = entry_path.len();
164            while let Some(slash_idx) = entry_path[..last_pos].rfind_byte(b'/') {
165                let dir = entry_path[..slash_idx].as_bstr();
166                last_pos = slash_idx;
167                let dir_range = entry.path.start..(entry.path.start + dir.len());
168
169                let hash = AccelerateLookup::icase_hash(dir);
170                if out
171                    .icase_dirs
172                    .find(hash, |dir| {
173                        dir.path(self) == self.path_backing[dir_range.clone()].as_bstr()
174                    })
175                    .is_none()
176                {
177                    out.icase_dirs.insert_unique(
178                        hash,
179                        crate::DirEntry {
180                            entry,
181                            dir_end: dir_range.end,
182                        },
183                        |dir| AccelerateLookup::icase_hash(dir.path(self)),
184                    );
185                } else {
186                    break;
187                }
188            }
189        }
190        gix_features::trace::debug!(directories = out.icase_dirs.len(), "stored directories");
191        out
192    }
193
194    /// Return the entry at `path` that is at the lowest available stage, using `lookup` for acceleration.
195    /// It must have been created from this instance, and was ideally kept up-to-date with it.
196    ///
197    /// If `ignore_case` is `true`, a case-insensitive (ASCII-folding only) search will be performed.
198    pub fn entry_by_path_icase<'a>(
199        &'a self,
200        path: &BStr,
201        ignore_case: bool,
202        lookup: &AccelerateLookup<'a>,
203    ) -> Option<&'a Entry> {
204        lookup
205            .icase_entries
206            .find(AccelerateLookup::icase_hash(path), |e| {
207                let entry_path = e.path(self);
208                if entry_path == path {
209                    return true;
210                }
211                if !ignore_case {
212                    return false;
213                }
214                entry_path.eq_ignore_ascii_case(path)
215            })
216            .copied()
217    }
218
219    /// Return the entry (at any stage) that is inside `directory`, or `None`,
220    /// or a directory itself like a submodule or sparse directory, using `lookup` for acceleration.
221    ///
222    /// If `ignore_case` is set, a case-insensitive (ASCII-folding only) search will be performed.
223    pub fn entry_closest_to_directory_or_directory_icase<'a>(
224        &'a self,
225        directory: &BStr,
226        ignore_case: bool,
227        lookup: &AccelerateLookup<'a>,
228    ) -> Option<&'a Entry> {
229        lookup
230            .icase_dirs
231            .find(AccelerateLookup::icase_hash(directory), |dir| {
232                let dir_path = dir.path(self);
233                if dir_path == directory {
234                    return true;
235                }
236                if !ignore_case {
237                    return false;
238                }
239                dir_path.eq_ignore_ascii_case(directory)
240            })
241            .map(|dir| dir.entry)
242    }
243
244    /// Return the entry (at any stage) that is inside `directory`, or `None`,
245    /// or that is a directory itself like a submodule or sparse directory.
246    ///
247    /// Note that this is a *case-sensitive* search.
248    pub fn entry_closest_to_directory_or_directory(&self, directory: &BStr) -> Option<&Entry> {
249        let idx = match self.entry_index_by_path(directory) {
250            Ok(idx) => {
251                let entry = &self.entries[idx];
252                return entry_is_dir(entry).then_some(entry);
253            }
254            Err(closest_idx) => closest_idx,
255        };
256        for entry in &self.entries[idx..] {
257            let path = entry.path(self);
258            if path.get(..directory.len())? != directory {
259                break;
260            }
261            let dir_char = path.get(directory.len())?;
262            if *dir_char > b'/' {
263                break;
264            }
265            if *dir_char < b'/' {
266                continue;
267            }
268            return Some(entry);
269        }
270        None
271    }
272
273    /// Check if `path` is a directory that contains entries in the index, or is a submodule.
274    ///
275    /// Returns `true` if there is at least one entry in the index whose path starts with `path/`,
276    /// indicating that `path` is a directory containing indexed files.
277    ///
278    /// For example, if the index contains an entry at `dirname/file`, then calling this method
279    /// with `dirname` would return `true`, but calling it with `dir` would return `false`.
280    ///
281    /// Note that this is a case-sensitive search.
282    pub fn path_is_directory(&self, path: &BStr) -> bool {
283        self.entry_closest_to_directory_or_directory(path).is_some()
284    }
285
286    /// Check if `path` is a directory that contains entries in the index or is a submodule,
287    /// with optional case-insensitive matching.
288    ///
289    /// Returns `true` if there is at least one entry in the index whose path starts with `path/`,
290    /// indicating that `path` is a directory containing indexed files.
291    ///
292    /// If `ignore_case` is `true`, a case-insensitive (ASCII-folding only) search will be performed.
293    ///
294    /// For example, if the index contains an entry at `dirname/file`, then calling this method
295    /// with `dirname` (or `DirName` with `ignore_case = true`) would return `true`, but calling it
296    /// with `dir` would return `false`.
297    pub fn path_is_directory_icase<'a>(
298        &'a self,
299        path: &BStr,
300        ignore_case: bool,
301        lookup: &AccelerateLookup<'a>,
302    ) -> bool {
303        self.entry_closest_to_directory_or_directory_icase(path, ignore_case, lookup)
304            .is_some()
305    }
306
307    /// Find the entry index in [`entries()[..upper_bound]`][State::entries()] matching the given repository-relative
308    /// `path` and `stage`, or `None`.
309    ///
310    /// Use the index for accessing multiple stages if they exists, but at least the single matching entry.
311    ///
312    /// # Panics
313    ///
314    /// If `upper_bound` is out of bounds of our entries array.
315    pub fn entry_index_by_path_and_stage_bounded(
316        &self,
317        path: &BStr,
318        stage: entry::Stage,
319        upper_bound: usize,
320    ) -> Option<usize> {
321        self.entries[..upper_bound]
322            .binary_search_by(|e| e.path(self).cmp(path).then_with(|| e.stage().cmp(&stage)))
323            .ok()
324    }
325
326    /// Like [`entry_index_by_path_and_stage()`](State::entry_index_by_path_and_stage()),
327    /// but returns the entry instead of the index.
328    pub fn entry_by_path_and_stage(&self, path: &BStr, stage: entry::Stage) -> Option<&Entry> {
329        self.entry_index_by_path_and_stage(path, stage)
330            .map(|idx| &self.entries[idx])
331    }
332
333    /// Return the entry at `path` that is either at stage 0, or at stage 2 (ours) in case of a merge conflict.
334    ///
335    /// Using this method is more efficient in comparison to doing two searches, one for stage 0 and one for stage 2.
336    pub fn entry_by_path(&self, path: &BStr) -> Option<&Entry> {
337        let mut stage_at_index = 0;
338        let idx = self
339            .entries
340            .binary_search_by(|e| {
341                let res = e.path(self).cmp(path);
342                if res.is_eq() {
343                    stage_at_index = e.stage_raw();
344                }
345                res
346            })
347            .ok()?;
348        let idx = if stage_at_index == 0 || stage_at_index == 2 {
349            idx
350        } else {
351            self.entry_index_by_idx_and_stage(path, idx, Stage::Ours as StageRaw, stage_at_index.cmp(&2))?
352        };
353        Some(&self.entries[idx])
354    }
355
356    /// Return the index at `Ok(index)` where the entry matching `path` (in any stage) can be found, or return
357    /// `Err(index)` to indicate the insertion position at which an entry with `path` would fit in.
358    pub fn entry_index_by_path(&self, path: &BStr) -> Result<usize, usize> {
359        self.entries.binary_search_by(|e| e.path(self).cmp(path))
360    }
361
362    /// Return the slice of entries which all share the same `prefix`, or `None` if there isn't a single such entry.
363    ///
364    /// If `prefix` is empty, all entries are returned.
365    pub fn prefixed_entries(&self, prefix: &BStr) -> Option<&[Entry]> {
366        self.prefixed_entries_range(prefix).map(|range| &self.entries[range])
367    }
368
369    /// Return the range of entries which all share the same `prefix`, or `None` if there isn't a single such entry.
370    ///
371    /// If `prefix` is empty, the range will include all entries.
372    pub fn prefixed_entries_range(&self, prefix: &BStr) -> Option<Range<usize>> {
373        if prefix.is_empty() {
374            return Some(0..self.entries.len());
375        }
376        let prefix_len = prefix.len();
377        let mut low = self.entries.partition_point(|e| {
378            e.path(self)
379                .get(..prefix_len)
380                .map_or_else(|| e.path(self) <= &prefix[..e.path.len()], |p| p < prefix)
381        });
382        let mut high =
383            low + self.entries[low..].partition_point(|e| e.path(self).get(..prefix_len).is_some_and(|p| p <= prefix));
384
385        let low_entry = &self.entries.get(low)?;
386        if low_entry.stage_raw() != 0 {
387            low = self
388                .walk_entry_stages(low_entry.path(self), low, Ordering::Less)
389                .unwrap_or(low);
390        }
391        if let Some(high_entry) = self.entries.get(high) {
392            if high_entry.stage_raw() != 0 {
393                high = self
394                    .walk_entry_stages(high_entry.path(self), high, Ordering::Less)
395                    .unwrap_or(high);
396            }
397        }
398        (low != high).then_some(low..high)
399    }
400
401    /// Return the entry at `idx` or _panic_ if the index is out of bounds.
402    ///
403    /// The `idx` is typically returned by [`entry_by_path_and_stage()`][State::entry_by_path_and_stage()].
404    pub fn entry(&self, idx: usize) -> &Entry {
405        &self.entries[idx]
406    }
407
408    /// Returns a boolean value indicating whether the index is sparse or not.
409    ///
410    /// An index is sparse if it contains at least one [`Mode::DIR`][entry::Mode::DIR] entry.
411    pub fn is_sparse(&self) -> bool {
412        self.is_sparse
413    }
414
415    /// Return the range of entries that exactly match the given `path`, in all available stages, or `None` if no entry with such
416    /// path exists.
417    ///
418    /// The range can be used to access the respective entries via [`entries()`](Self::entries()) or [`entries_mut()](Self::entries_mut()).
419    pub fn entry_range(&self, path: &BStr) -> Option<Range<usize>> {
420        let mut stage_at_index = 0;
421        let idx = self
422            .entries
423            .binary_search_by(|e| {
424                let res = e.path(self).cmp(path);
425                if res.is_eq() {
426                    stage_at_index = e.stage_raw();
427                }
428                res
429            })
430            .ok()?;
431
432        let (start, end) = (
433            self.walk_entry_stages(path, idx, Ordering::Less).unwrap_or(idx),
434            self.walk_entry_stages(path, idx, Ordering::Greater).unwrap_or(idx) + 1,
435        );
436        Some(start..end)
437    }
438}
439
440impl AccelerateLookup<'_> {
441    fn with_capacity(cap: usize) -> Self {
442        let ratio_of_entries_to_dirs_in_webkit = 20; // 400k entries and 20k dirs
443        Self {
444            icase_entries: hashbrown::HashTable::with_capacity(cap),
445            icase_dirs: hashbrown::HashTable::with_capacity(cap / ratio_of_entries_to_dirs_in_webkit),
446        }
447    }
448    fn icase_hash(data: &BStr) -> u64 {
449        use std::hash::Hasher;
450        let mut hasher = fnv::FnvHasher::default();
451        for b in data.as_bytes() {
452            hasher.write_u8(b.to_ascii_lowercase());
453        }
454        hasher.finish()
455    }
456}
457
458/// Mutation
459impl State {
460    /// After usage of the storage obtained by [`take_path_backing()`][Self::take_path_backing()], return it here.
461    /// Note that it must not be empty.
462    pub fn return_path_backing(&mut self, backing: PathStorage) {
463        debug_assert!(
464            self.path_backing.is_empty(),
465            "BUG: return path backing only after taking it, once"
466        );
467        self.path_backing = backing;
468    }
469
470    /// Return mutable entries in a slice.
471    pub fn entries_mut(&mut self) -> &mut [Entry] {
472        &mut self.entries
473    }
474
475    /// Return a writable slice to entries and read-access to their path storage at the same time.
476    pub fn entries_mut_and_pathbacking(&mut self) -> (&mut [Entry], &PathStorageRef) {
477        (&mut self.entries, &self.path_backing)
478    }
479
480    /// Return mutable entries along with their paths in an iterator.
481    pub fn entries_mut_with_paths(&mut self) -> impl Iterator<Item = (&mut Entry, &BStr)> {
482        let paths = &self.path_backing;
483        self.entries.iter_mut().map(move |e| {
484            let path = paths[e.path.clone()].as_bstr();
485            (e, path)
486        })
487    }
488
489    /// Return all parts that relate to entries, which includes path storage.
490    ///
491    /// This can be useful for obtaining a standalone, boxable iterator
492    pub fn into_entries(self) -> (Vec<Entry>, PathStorage) {
493        (self.entries, self.path_backing)
494    }
495
496    /// Sometimes it's needed to remove the path backing to allow certain mutation to happen in the state while supporting reading the entry's
497    /// path.
498    pub fn take_path_backing(&mut self) -> PathStorage {
499        assert_eq!(
500            self.entries.is_empty(),
501            self.path_backing.is_empty(),
502            "BUG: cannot take out backing multiple times"
503        );
504        std::mem::take(&mut self.path_backing)
505    }
506
507    /// Like [`entry_index_by_path_and_stage()`][State::entry_index_by_path_and_stage()],
508    /// but returns the mutable entry instead of the index.
509    pub fn entry_mut_by_path_and_stage(&mut self, path: &BStr, stage: entry::Stage) -> Option<&mut Entry> {
510        self.entry_index_by_path_and_stage(path, stage)
511            .map(|idx| &mut self.entries[idx])
512    }
513
514    /// Push a new entry containing `stat`, `id`, `flags` and `mode` and `path` to the end of our storage, without performing
515    /// any sanity checks. This means it's possible to push a new entry to the same path on the same stage and even after sorting
516    /// the entries lookups may still return the wrong one of them unless the correct binary search criteria is chosen.
517    ///
518    /// Note that this *is likely* to break invariants that will prevent further lookups by path unless
519    /// [`entry_index_by_path_and_stage_bounded()`][State::entry_index_by_path_and_stage_bounded()] is used with
520    /// the `upper_bound` being the amount of entries before the first call to this method.
521    ///
522    /// Alternatively, make sure to call [`sort_entries()`][State::sort_entries()] before entry lookup by path to restore
523    /// the invariant.
524    pub fn dangerously_push_entry(
525        &mut self,
526        stat: entry::Stat,
527        id: gix_hash::ObjectId,
528        flags: entry::Flags,
529        mode: entry::Mode,
530        path: &BStr,
531    ) {
532        let path = {
533            let path_start = self.path_backing.len();
534            self.path_backing.push_str(path);
535            path_start..self.path_backing.len()
536        };
537
538        self.entries.push(Entry {
539            stat,
540            id,
541            flags,
542            mode,
543            path,
544        });
545    }
546
547    /// Unconditionally sort entries as needed to perform lookups quickly.
548    pub fn sort_entries(&mut self) {
549        let path_backing = &self.path_backing;
550        self.entries.sort_by(|a, b| {
551            Entry::cmp_filepaths(a.path_in(path_backing), b.path_in(path_backing))
552                .then_with(|| a.stage().cmp(&b.stage()))
553        });
554    }
555
556    /// Similar to [`sort_entries()`][State::sort_entries()], but applies `compare` after comparing
557    /// by path and stage as a third criteria.
558    pub fn sort_entries_by(&mut self, mut compare: impl FnMut(&Entry, &Entry) -> Ordering) {
559        let path_backing = &self.path_backing;
560        self.entries.sort_by(|a, b| {
561            Entry::cmp_filepaths(a.path_in(path_backing), b.path_in(path_backing))
562                .then_with(|| a.stage().cmp(&b.stage()))
563                .then_with(|| compare(a, b))
564        });
565    }
566
567    /// Physically remove all entries for which `should_remove(idx, path, entry)` returns `true`, traversing them from first to last.
568    ///
569    /// Note that the memory used for the removed entries paths is not freed, as it's append-only, and
570    /// that some extensions might refer to paths which are now deleted.
571    ///
572    /// ### Performance
573    ///
574    /// To implement this operation typically, one would rather add [entry::Flags::REMOVE] to each entry to remove
575    /// them when [writing the index](Self::write_to()).
576    pub fn remove_entries(&mut self, mut should_remove: impl FnMut(usize, &BStr, &mut Entry) -> bool) {
577        let mut index = 0;
578        let paths = &self.path_backing;
579        self.entries.retain_mut(|e| {
580            let path = e.path_in(paths);
581            let res = !should_remove(index, path, e);
582            index += 1;
583            res
584        });
585    }
586
587    /// Physically remove the entry at `index`, or panic if the entry didn't exist.
588    ///
589    /// This call is typically made after looking up `index`, so it's clear that it will not panic.
590    ///
591    /// Note that the memory used for the removed entries paths is not freed, as it's append-only, and
592    /// that some extensions might refer to paths which are now deleted.
593    pub fn remove_entry_at_index(&mut self, index: usize) -> Entry {
594        self.entries.remove(index)
595    }
596}
597
598/// Extensions
599impl State {
600    /// Access the `tree` extension.
601    pub fn tree(&self) -> Option<&extension::Tree> {
602        self.tree.as_ref()
603    }
604    /// Remove the `tree` extension.
605    pub fn remove_tree(&mut self) -> Option<extension::Tree> {
606        self.tree.take()
607    }
608    /// Access the `link` extension.
609    pub fn link(&self) -> Option<&extension::Link> {
610        self.link.as_ref()
611    }
612    /// Obtain the resolve-undo extension.
613    pub fn resolve_undo(&self) -> Option<&extension::resolve_undo::Paths> {
614        self.resolve_undo.as_ref()
615    }
616    /// Remove the resolve-undo extension.
617    pub fn remove_resolve_undo(&mut self) -> Option<extension::resolve_undo::Paths> {
618        self.resolve_undo.take()
619    }
620    /// Obtain the untracked extension.
621    pub fn untracked(&self) -> Option<&extension::UntrackedCache> {
622        self.untracked.as_ref()
623    }
624    /// Obtain the fsmonitor extension.
625    pub fn fs_monitor(&self) -> Option<&extension::FsMonitor> {
626        self.fs_monitor.as_ref()
627    }
628    /// Return `true` if the end-of-index extension was present when decoding this index.
629    pub fn had_end_of_index_marker(&self) -> bool {
630        self.end_of_index_at_decode_time
631    }
632    /// Return `true` if the offset-table extension was present when decoding this index.
633    pub fn had_offset_table(&self) -> bool {
634        self.offset_table_at_decode_time
635    }
636}
637
638fn entry_is_dir(entry: &Entry) -> bool {
639    entry.mode.is_sparse() || entry.mode.is_submodule()
640}
641
642#[cfg(test)]
643mod tests {
644    use std::path::{Path, PathBuf};
645
646    #[test]
647    fn entry_by_path_with_conflicting_file() {
648        let file = PathBuf::from("tests")
649            .join("fixtures")
650            .join(Path::new("loose_index").join("conflicting-file.git-index"));
651        let file = crate::File::at(file, gix_hash::Kind::Sha1, false, Default::default()).expect("valid file");
652        assert_eq!(
653            file.entries().len(),
654            3,
655            "we have a set of conflict entries for a single file"
656        );
657        for idx in 0..3 {
658            for wanted_stage in 1..=3 {
659                let actual_idx = file
660                    .entry_index_by_idx_and_stage(
661                        "file".into(),
662                        idx,
663                        wanted_stage,
664                        (idx + 1).cmp(&(wanted_stage as usize)),
665                    )
666                    .expect("found");
667                assert_eq!(
668                    actual_idx + 1,
669                    wanted_stage as usize,
670                    "the index and stage have a relation, and that is upheld if we search correctly"
671                );
672            }
673        }
674    }
675}