diff --git a/gitoxide-core/src/index/information.rs b/gitoxide-core/src/index/information.rs index aa618ab10c3..4540e5bc832 100644 --- a/gitoxide-core/src/index/information.rs +++ b/gitoxide-core/src/index/information.rs @@ -6,6 +6,7 @@ pub struct Options { #[cfg(feature = "serde")] mod serde_only { + use gix::index::entry::Stage; mod ext { #[derive(serde::Serialize)] @@ -115,11 +116,10 @@ mod serde_only { let (mut intent_to_add, mut skip_worktree) = (0, 0); for entry in f.entries() { match entry.flags.stage() { - 0 => stage_0_merged += 1, - 1 => stage_1_base += 1, - 2 => stage_2_ours += 1, - 3 => stage_3_theirs += 1, - invalid => anyhow::bail!("Invalid stage {} encountered", invalid), + Stage::Unconflicted => stage_0_merged += 1, + Stage::Base => stage_1_base += 1, + Stage::Ours => stage_2_ours += 1, + Stage::Theirs => stage_3_theirs += 1, } match entry.mode { gix::index::entry::Mode::DIR => dir += 1, diff --git a/gitoxide-core/src/repository/attributes/validate_baseline.rs b/gitoxide-core/src/repository/attributes/validate_baseline.rs index f2c92f71a30..a5571d45e86 100644 --- a/gitoxide-core/src/repository/attributes/validate_baseline.rs +++ b/gitoxide-core/src/repository/attributes/validate_baseline.rs @@ -262,6 +262,8 @@ pub(crate) mod function { } #[derive(Debug)] + // See note on `Mismatch` + #[allow(dead_code)] pub struct ExcludeLocation { pub line: usize, pub rela_source_file: String, @@ -269,6 +271,9 @@ pub(crate) mod function { } #[derive(Debug)] + // We debug-print this structure, which makes all fields 'used', but it doesn't count. + // TODO: find a way to not have to do more work, but make the warning go away. + #[allow(dead_code)] pub enum Mismatch { Attributes { actual: Vec, @@ -281,6 +286,8 @@ pub(crate) mod function { } #[derive(Debug)] + // See note on `Mismatch` + #[allow(dead_code)] pub struct ExcludeMatch { pub pattern: gix::glob::Pattern, pub source: Option, diff --git a/gitoxide-core/src/repository/index/entries.rs b/gitoxide-core/src/repository/index/entries.rs index 7126849ad4d..3f17d55b3c3 100644 --- a/gitoxide-core/src/repository/index/entries.rs +++ b/gitoxide-core/src/repository/index/entries.rs @@ -23,6 +23,7 @@ pub(crate) mod function { io::{BufWriter, Write}, }; + use gix::index::entry::Stage; use gix::{ bstr::{BStr, BString}, worktree::IndexPersistedOrInMemory, @@ -392,11 +393,10 @@ pub(crate) mod function { out, "{} {}{:?} {} {}{}{}", match entry.flags.stage() { - 0 => " ", - 1 => "BASE ", - 2 => "OURS ", - 3 => "THEIRS ", - _ => "UNKNOWN", + Stage::Unconflicted => " ", + Stage::Base => "BASE ", + Stage::Ours => "OURS ", + Stage::Theirs => "THEIRS ", }, if entry.flags.is_empty() { "".to_string() diff --git a/gix-config/src/file/includes/mod.rs b/gix-config/src/file/includes/mod.rs index 8fd92725fba..98a68482558 100644 --- a/gix-config/src/file/includes/mod.rs +++ b/gix-config/src/file/includes/mod.rs @@ -99,7 +99,14 @@ fn append_followed_includes_recursively( } buf.clear(); - std::io::copy(&mut std::fs::File::open(&config_path)?, buf)?; + std::io::copy( + &mut std::fs::File::open(&config_path).map_err(|err| Error::Io { + source: err, + path: config_path.to_owned(), + })?, + buf, + ) + .map_err(Error::CopyBuffer)?; let config_meta = Metadata { path: Some(config_path), trust: meta.trust, diff --git a/gix-config/src/file/includes/types.rs b/gix-config/src/file/includes/types.rs index 54b1eb5bfe0..85e3a35db89 100644 --- a/gix-config/src/file/includes/types.rs +++ b/gix-config/src/file/includes/types.rs @@ -1,11 +1,14 @@ use crate::{parse, path::interpolate}; +use std::path::PathBuf; /// The error returned when following includes. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { - #[error(transparent)] - Io(#[from] std::io::Error), + #[error("Failed to copy configuration file into buffer")] + CopyBuffer(#[source] std::io::Error), + #[error("Could not read included configuration file at '{}'", path.display())] + Io { path: PathBuf, source: std::io::Error }, #[error(transparent)] Parse(#[from] parse::Error), #[error(transparent)] diff --git a/gix-index/src/access/mod.rs b/gix-index/src/access/mod.rs index d5da3c9934a..dd2449c6d47 100644 --- a/gix-index/src/access/mod.rs +++ b/gix-index/src/access/mod.rs @@ -3,6 +3,7 @@ use std::{cmp::Ordering, ops::Range}; use bstr::{BStr, ByteSlice, ByteVec}; use filetime::FileTime; +use crate::entry::{Stage, StageRaw}; use crate::{entry, extension, AccelerateLookup, Entry, PathStorage, PathStorageRef, State, Version}; // TODO: integrate this somehow, somewhere, depending on later usage. @@ -81,7 +82,7 @@ impl State { res }) .ok()?; - self.entry_index_by_idx_and_stage(path, idx, stage, stage_cmp) + self.entry_index_by_idx_and_stage(path, idx, stage as StageRaw, stage_cmp) } /// Walk as far in `direction` as possible, with [`Ordering::Greater`] towards higher stages, and [`Ordering::Less`] @@ -112,7 +113,7 @@ impl State { &self, path: &BStr, idx: usize, - wanted_stage: entry::Stage, + wanted_stage: entry::StageRaw, stage_cmp: Ordering, ) -> Option { match stage_cmp { @@ -121,7 +122,7 @@ impl State { .enumerate() .rev() .take_while(|(_, e)| e.path(self) == path) - .find_map(|(idx, e)| (e.stage() == wanted_stage).then_some(idx)), + .find_map(|(idx, e)| (e.stage_raw() == wanted_stage).then_some(idx)), Ordering::Equal => Some(idx), Ordering::Less => self .entries @@ -129,7 +130,7 @@ impl State { .iter() .enumerate() .take_while(|(_, e)| e.path(self) == path) - .find_map(|(ofs, e)| (e.stage() == wanted_stage).then_some(idx + ofs + 1)), + .find_map(|(ofs, e)| (e.stage_raw() == wanted_stage).then_some(idx + ofs + 1)), } } @@ -291,7 +292,7 @@ impl State { .binary_search_by(|e| { let res = e.path(self).cmp(path); if res.is_eq() { - stage_at_index = e.stage(); + stage_at_index = e.stage_raw(); } res }) @@ -299,7 +300,7 @@ impl State { let idx = if stage_at_index == 0 || stage_at_index == 2 { idx } else { - self.entry_index_by_idx_and_stage(path, idx, 2, stage_at_index.cmp(&2))? + self.entry_index_by_idx_and_stage(path, idx, Stage::Ours as StageRaw, stage_at_index.cmp(&2))? }; Some(&self.entries[idx]) } @@ -334,13 +335,13 @@ impl State { + self.entries[low..].partition_point(|e| e.path(self).get(..prefix_len).map_or(false, |p| p <= prefix)); let low_entry = &self.entries.get(low)?; - if low_entry.stage() != 0 { + if low_entry.stage_raw() != 0 { low = self .walk_entry_stages(low_entry.path(self), low, Ordering::Less) .unwrap_or(low); } if let Some(high_entry) = self.entries.get(high) { - if high_entry.stage() != 0 { + if high_entry.stage_raw() != 0 { high = self .walk_entry_stages(high_entry.path(self), high, Ordering::Less) .unwrap_or(high); @@ -374,7 +375,7 @@ impl State { .binary_search_by(|e| { let res = e.path(self).cmp(path); if res.is_eq() { - stage_at_index = e.stage(); + stage_at_index = e.stage_raw(); } res }) diff --git a/gix-index/src/entry/flags.rs b/gix-index/src/entry/flags.rs index 05198bafb01..c32cb6f8d7a 100644 --- a/gix-index/src/entry/flags.rs +++ b/gix-index/src/entry/flags.rs @@ -62,6 +62,23 @@ bitflags! { impl Flags { /// Return the stage as extracted from the bits of this instance. pub fn stage(&self) -> Stage { + match self.stage_raw() { + 0 => Stage::Unconflicted, + 1 => Stage::Base, + 2 => Stage::Ours, + 3 => Stage::Theirs, + _ => unreachable!("BUG: Flags::STAGE_MASK is two bits, whose 4 possible values we have covered"), + } + } + + /// Return an entry's stage as raw number between 0 and 4. + /// Possible values are: + /// + /// * 0 = no conflict, + /// * 1 = base, + /// * 2 = ours, + /// * 3 = theirs + pub fn stage_raw(&self) -> u32 { (*self & Flags::STAGE_MASK).bits() >> 12 } diff --git a/gix-index/src/entry/mod.rs b/gix-index/src/entry/mod.rs index 75c8fa67121..5fd2e501930 100644 --- a/gix-index/src/entry/mod.rs +++ b/gix-index/src/entry/mod.rs @@ -1,9 +1,23 @@ -/// The stage of an entry, one of… +/// The stage of an entry. +#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub enum Stage { + /// This is the default, and most entries are in this stage. + #[default] + Unconflicted = 0, + /// The entry is the common base between 'our' change and 'their' change, for comparison. + Base = 1, + /// The entry represents our change. + Ours = 2, + /// The entry represents their change. + Theirs = 3, +} + +// The stage of an entry, one of… /// * 0 = no conflict, /// * 1 = base, /// * 2 = ours, /// * 3 = theirs -pub type Stage = u32; +pub type StageRaw = u32; /// #[allow(clippy::empty_docs)] @@ -78,6 +92,17 @@ mod access { pub fn stage(&self) -> entry::Stage { self.flags.stage() } + + /// Return an entry's stage as raw number between 0 and 4. + /// Possible values are: + /// + /// * 0 = no conflict, + /// * 1 = base, + /// * 2 = ours, + /// * 3 = theirs + pub fn stage_raw(&self) -> u32 { + self.flags.stage_raw() + } } } diff --git a/gix-index/src/extension/fs_monitor.rs b/gix-index/src/extension/fs_monitor.rs index 4bb5016ffe5..79d609dba19 100644 --- a/gix-index/src/extension/fs_monitor.rs +++ b/gix-index/src/extension/fs_monitor.rs @@ -6,6 +6,7 @@ use crate::{ }; #[derive(Clone)] +#[allow(dead_code)] pub enum Token { V1 { nanos_since_1970: u64 }, V2 { token: BString }, diff --git a/gix-index/src/lib.rs b/gix-index/src/lib.rs index 580b8e59718..a8ff94c0369 100644 --- a/gix-index/src/lib.rs +++ b/gix-index/src/lib.rs @@ -144,6 +144,7 @@ pub struct State { } mod impls { + use crate::entry::Stage; use std::fmt::{Debug, Formatter}; use crate::State; @@ -155,11 +156,10 @@ mod impls { f, "{} {}{:?} {} {}", match entry.flags.stage() { - 0 => " ", - 1 => "BASE ", - 2 => "OURS ", - 3 => "THEIRS ", - _ => "UNKNOWN", + Stage::Unconflicted => " ", + Stage::Base => "BASE ", + Stage::Ours => "OURS ", + Stage::Theirs => "THEIRS ", }, if entry.flags.is_empty() { "".to_string() diff --git a/gix-index/tests/index/access.rs b/gix-index/tests/index/access.rs index af76a45a295..d3e720486a4 100644 --- a/gix-index/tests/index/access.rs +++ b/gix-index/tests/index/access.rs @@ -1,5 +1,6 @@ use crate::index::Fixture; use bstr::{BString, ByteSlice}; +use gix_index::entry::Stage; fn icase_fixture() -> gix_index::File { Fixture::Generated("v2_icase_name_clashes").open() @@ -11,7 +12,7 @@ fn entry_by_path() { for entry in file.entries() { let path = entry.path(&file); assert_eq!(file.entry_by_path(path), Some(entry)); - assert_eq!(file.entry_by_path_and_stage(path, 0), Some(entry)); + assert_eq!(file.entry_by_path_and_stage(path, Stage::Unconflicted), Some(entry)); } } @@ -140,27 +141,27 @@ fn entry_by_path_and_stage() { for entry in file.entries() { let path = entry.path(&file); assert_eq!( - file.entry_index_by_path_and_stage(path, 0) + file.entry_index_by_path_and_stage(path, Stage::Unconflicted) .map(|idx| &file.entries()[idx]), Some(entry) ); - assert_eq!(file.entry_by_path_and_stage(path, 0), Some(entry)); + assert_eq!(file.entry_by_path_and_stage(path, Stage::Unconflicted), Some(entry)); } } #[test] fn entry_by_path_with_conflicting_file() { let file = Fixture::Loose("conflicting-file").open(); - for expected_stage in [1 /* common ancestor */, 2 /* ours */, 3 /* theirs */] { + for expected_stage in [Stage::Base, Stage::Ours, Stage::Theirs] { assert!( file.entry_by_path_and_stage("file".into(), expected_stage).is_some(), - "we have no stage 0 during a conflict, but all other ones. Missed {expected_stage}" + "we have no stage 0 during a conflict, but all other ones. Missed {expected_stage:?}" ); } assert_eq!( file.entry_by_path("file".into()).expect("found").stage(), - 2, + Stage::Ours, "we always find our stage while in a merge" ); } @@ -226,13 +227,13 @@ fn sort_entries() { for (idx, entry) in file.entries()[..valid_entries].iter().enumerate() { assert_eq!( - file.entry_index_by_path_and_stage_bounded(entry.path(&file), 0, valid_entries), + file.entry_index_by_path_and_stage_bounded(entry.path(&file), Stage::Unconflicted, valid_entries), Some(idx), "we can still find entries in the correctly sorted region" ); } assert_eq!( - file.entry_by_path_and_stage(new_entry_path, 0), + file.entry_by_path_and_stage(new_entry_path, Stage::Unconflicted), None, "new entry can't be found due to incorrect order" ); @@ -241,7 +242,7 @@ fn sort_entries() { assert!(file.verify_entries().is_ok(), "sorting of entries restores invariants"); assert_eq!( - file.entry_by_path_and_stage(new_entry_path, 0) + file.entry_by_path_and_stage(new_entry_path, Stage::Unconflicted) .expect("can be found") .path(&file), new_entry_path, diff --git a/gix-status/src/index_as_worktree/function.rs b/gix-status/src/index_as_worktree/function.rs index e69d6629b64..dbe7a838ed2 100644 --- a/gix-status/src/index_as_worktree/function.rs +++ b/gix-status/src/index_as_worktree/function.rs @@ -157,11 +157,11 @@ where let mut idx = 0; while let Some(entry) = chunk_entries.get(idx) { let absolute_entry_index = entry_offset + idx; - if idx == 0 && entry.stage() != 0 { + if idx == 0 && entry.stage_raw() != 0 { let offset = entry_offset.checked_sub(1).and_then(|prev_idx| { let prev_entry = &all_entries[prev_idx]; let entry_path = entry.path_in(state.path_backing); - if prev_entry.stage() == 0 || prev_entry.path_in(state.path_backing) != entry_path { + if prev_entry.stage_raw() == 0 || prev_entry.path_in(state.path_backing) != entry_path { // prev_entry (in previous chunk) does not belong to our conflict return None; } @@ -286,7 +286,7 @@ impl<'index> State<'_, 'index> { self.skipped_by_pathspec.fetch_add(1, Ordering::Relaxed); return None; } - let status = if entry.stage() != 0 { + let status = if entry.stage_raw() != 0 { Ok( Conflict::try_from_entry(entries, self.path_backing, entry_index, path).map(|(conflict, offset)| { *outer_entry_index += offset; // let out loop skip over entries related to the conflict @@ -604,7 +604,7 @@ impl Conflict { let mut count = 0_usize; for stage in (start_index..(start_index + 3).min(entries.len())).filter_map(|idx| { let entry = &entries[idx]; - let stage = entry.stage(); + let stage = entry.stage_raw(); (stage > 0 && entry.path_in(path_backing) == entry_path).then_some(stage) }) { // This could be `1 << (stage - 1)` but let's be specific. diff --git a/gix-worktree/src/stack/platform.rs b/gix-worktree/src/stack/platform.rs index 0fcdf90f216..0b270516d24 100644 --- a/gix-worktree/src/stack/platform.rs +++ b/gix-worktree/src/stack/platform.rs @@ -20,6 +20,7 @@ impl<'a> Platform<'a> { /// # Panics /// /// If the cache was configured without exclude patterns. + #[doc(alias = "is_path_ignored", alias = "git2")] pub fn is_excluded(&self) -> bool { self.matching_exclude_pattern() .map_or(false, |m| !m.pattern.is_negative()) diff --git a/gix-worktree/src/stack/state/mod.rs b/gix-worktree/src/stack/state/mod.rs index ba6383fee85..04afb046368 100644 --- a/gix-worktree/src/stack/state/mod.rs +++ b/gix-worktree/src/stack/state/mod.rs @@ -131,7 +131,7 @@ impl State { // Stage 0 means there is no merge going on, stage 2 means it's 'our' side of the merge, but then // there won't be a stage 0. - if entry.mode == gix_index::entry::Mode::FILE && (entry.stage() == 0 || entry.stage() == 2) { + if entry.mode == gix_index::entry::Mode::FILE && (entry.stage_raw() == 0 || entry.stage_raw() == 2) { let basename = path.rfind_byte(b'/').map_or(path, |pos| path[pos + 1..].as_bstr()); let ignore_source = names.iter().find_map(|t| { match case { diff --git a/gix/src/attribute_stack.rs b/gix/src/attribute_stack.rs index 66497def6ae..e2b9ecc5ce6 100644 --- a/gix/src/attribute_stack.rs +++ b/gix/src/attribute_stack.rs @@ -39,6 +39,7 @@ impl<'repo> AttributeStack<'repo> { /// path is created as directory. If it's not known it is assumed to be a file. /// /// Provide access to cached information for that `relative` path via the returned platform. + #[doc(alias = "is_path_ignored", alias = "git2")] pub fn at_path( &mut self, relative: impl AsRef, diff --git a/gix/src/repository/attributes.rs b/gix/src/repository/attributes.rs index 7f747f7fd98..06bcb569970 100644 --- a/gix/src/repository/attributes.rs +++ b/gix/src/repository/attributes.rs @@ -106,6 +106,7 @@ impl Repository { /// When only excludes are desired, this is the most efficient way to obtain them. Otherwise use /// [`Repository::attributes()`] for accessing both attributes and excludes. // TODO: test + #[doc(alias = "is_path_ignored", alias = "git2")] #[cfg(feature = "excludes")] pub fn excludes( &self, diff --git a/gix/src/repository/index.rs b/gix/src/repository/index.rs index 18899d09fba..9d2ba0ccfb1 100644 --- a/gix/src/repository/index.rs +++ b/gix/src/repository/index.rs @@ -40,6 +40,11 @@ impl crate::Repository { /// Return a shared worktree index which is updated automatically if the in-memory snapshot has become stale as the underlying file /// on disk has changed. /// + /// ### Notes + /// + /// * This will fail if the file doesn't exist, like in a newly initialized repository. If that is the case, use + /// [index_or_empty()](Self::index_or_empty) or [try_index()](Self::try_index) instead. + /// /// The index file is shared across all clones of this repository. pub fn index(&self) -> Result { self.try_index().and_then(|opt| match opt { diff --git a/gix/src/revision/spec/parse/delegate/navigate.rs b/gix/src/revision/spec/parse/delegate/navigate.rs index 731a24136d3..acd8ecc530b 100644 --- a/gix/src/revision/spec/parse/delegate/navigate.rs +++ b/gix/src/revision/spec/parse/delegate/navigate.rs @@ -1,6 +1,7 @@ use std::collections::HashSet; use gix_hash::ObjectId; +use gix_index::entry::Stage; use gix_revision::spec::parse::{ delegate, delegate::{PeelTo, Traversal}, @@ -305,9 +306,18 @@ impl<'repo> delegate::Navigate for Delegate<'repo> { } fn index_lookup(&mut self, path: &BStr, stage: u8) -> Option<()> { + let stage = match stage { + 0 => Stage::Unconflicted, + 1 => Stage::Base, + 2 => Stage::Ours, + 3 => Stage::Theirs, + _ => unreachable!( + "BUG: driver will not pass invalid stages (and it uses integer to avoid gix-index as dependency)" + ), + }; self.unset_disambiguate_call(); match self.repo.index() { - Ok(index) => match index.entry_by_path_and_stage(path, stage.into()) { + Ok(index) => match index.entry_by_path_and_stage(path, stage) { Some(entry) => { self.objs[self.idx] .get_or_insert_with(HashSet::default) @@ -323,21 +333,17 @@ impl<'repo> delegate::Navigate for Delegate<'repo> { Some(()) } None => { - let stage_hint = [0, 1, 2] + let stage_hint = [Stage::Unconflicted, Stage::Base, Stage::Ours] .iter() .filter(|our_stage| **our_stage != stage) - .find_map(|stage| { - index - .entry_index_by_path_and_stage(path, (*stage).into()) - .map(|_| (*stage).into()) - }); + .find_map(|stage| index.entry_index_by_path_and_stage(path, *stage).map(|_| *stage)); let exists = self .repo .work_dir() .map_or(false, |root| root.join(gix_path::from_bstr(path)).exists()); self.err.push(Error::IndexLookup { desired_path: path.into(), - desired_stage: stage.into(), + desired_stage: stage, exists, stage_hint, }); diff --git a/gix/src/revision/spec/parse/types.rs b/gix/src/revision/spec/parse/types.rs index 297ab5b467b..fc09e13c097 100644 --- a/gix/src/revision/spec/parse/types.rs +++ b/gix/src/revision/spec/parse/types.rs @@ -98,7 +98,7 @@ pub enum Error { desired: usize, available: usize, }, - #[error("Path {desired_path:?} did not exist in index at stage {desired_stage}{}{}", stage_hint.map(|actual|format!(". It does exist at stage {actual}")).unwrap_or_default(), exists.then(|| ". It exists on disk").unwrap_or(". It does not exist on disk"))] + #[error("Path {desired_path:?} did not exist in index at stage {}{}{}", *desired_stage as u8, stage_hint.map(|actual|format!(". It does exist at stage {}", actual as u8)).unwrap_or_default(), exists.then(|| ". It exists on disk").unwrap_or(". It does not exist on disk"))] IndexLookup { desired_path: BString, desired_stage: gix_index::entry::Stage, diff --git a/gix/src/status/index_worktree.rs b/gix/src/status/index_worktree.rs index d95ab5a2438..c658e0e2ee2 100644 --- a/gix/src/status/index_worktree.rs +++ b/gix/src/status/index_worktree.rs @@ -199,10 +199,7 @@ mod submodule_status { let local_repo = repo.to_thread_local(); let submodule_paths = match local_repo.submodules()? { Some(sm) => { - let mut v: Vec<_> = sm - .filter(|sm| sm.is_active().unwrap_or_default()) - .filter_map(|sm| sm.path().ok().map(Cow::into_owned)) - .collect(); + let mut v: Vec<_> = sm.filter_map(|sm| sm.path().ok().map(Cow::into_owned)).collect(); v.sort(); v } @@ -271,7 +268,8 @@ mod submodule_status { /// /// ### Submodules /// -/// Note that submodules can be set to 'inactive' which automatically excludes them from the status operation. +/// Note that submodules can be set to 'inactive', which will not exclude them from the status operation, similar to +/// how `git status` includes them. /// /// ### Index Changes /// @@ -613,7 +611,10 @@ pub mod iter { /// /// * `patterns` /// - Optional patterns to use to limit the paths to look at. If empty, all paths are considered. - pub fn into_index_worktree_iter(self, patterns: Vec) -> Result { + pub fn into_index_worktree_iter( + self, + patterns: impl IntoIterator, + ) -> Result { let index = match self.index { None => IndexPersistedOrInMemory::Persisted(self.repo.index_or_empty()?), Some(index) => index, @@ -634,6 +635,7 @@ pub mod iter { { let (tx, rx) = std::sync::mpsc::channel(); let mut collect = Collect { tx }; + let patterns: Vec<_> = patterns.into_iter().collect(); let join = std::thread::Builder::new() .name("gix::status::index_worktree::iter::producer".into()) .spawn({