Skip to content

tree ➡ index diff for status #1317

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 7, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions gitoxide-core/src/index/information.rs
Original file line number Diff line number Diff line change
@@ -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,
7 changes: 7 additions & 0 deletions gitoxide-core/src/repository/attributes/validate_baseline.rs
Original file line number Diff line number Diff line change
@@ -262,13 +262,18 @@ pub(crate) mod function {
}

#[derive(Debug)]
// See note on `Mismatch`
#[allow(dead_code)]
pub struct ExcludeLocation {
pub line: usize,
pub rela_source_file: String,
pub pattern: String,
}

#[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<gix::attrs::Assignment>,
@@ -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<PathBuf>,
10 changes: 5 additions & 5 deletions gitoxide-core/src/repository/index/entries.rs
Original file line number Diff line number Diff line change
@@ -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()
9 changes: 8 additions & 1 deletion gix-config/src/file/includes/mod.rs
Original file line number Diff line number Diff line change
@@ -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,
7 changes: 5 additions & 2 deletions gix-config/src/file/includes/types.rs
Original file line number Diff line number Diff line change
@@ -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)]
19 changes: 10 additions & 9 deletions gix-index/src/access/mod.rs
Original file line number Diff line number Diff line change
@@ -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<usize> {
match stage_cmp {
@@ -121,15 +122,15 @@ 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
.get(idx + 1..)?
.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,15 +292,15 @@ 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
})
.ok()?;
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
})
17 changes: 17 additions & 0 deletions gix-index/src/entry/flags.rs
Original file line number Diff line number Diff line change
@@ -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
}

29 changes: 27 additions & 2 deletions gix-index/src/entry/mod.rs
Original file line number Diff line number Diff line change
@@ -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()
}
}
}

1 change: 1 addition & 0 deletions gix-index/src/extension/fs_monitor.rs
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ use crate::{
};

#[derive(Clone)]
#[allow(dead_code)]
pub enum Token {
V1 { nanos_since_1970: u64 },
V2 { token: BString },
10 changes: 5 additions & 5 deletions gix-index/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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()
19 changes: 10 additions & 9 deletions gix-index/tests/index/access.rs
Original file line number Diff line number Diff line change
@@ -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,
8 changes: 4 additions & 4 deletions gix-status/src/index_as_worktree/function.rs
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions gix-worktree/src/stack/platform.rs
Original file line number Diff line number Diff line change
@@ -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())
2 changes: 1 addition & 1 deletion gix-worktree/src/stack/state/mod.rs
Original file line number Diff line number Diff line change
@@ -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 {
1 change: 1 addition & 0 deletions gix/src/attribute_stack.rs
Original file line number Diff line number Diff line change
@@ -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<std::path::Path>,
1 change: 1 addition & 0 deletions gix/src/repository/attributes.rs
Original file line number Diff line number Diff line change
@@ -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,
5 changes: 5 additions & 0 deletions gix/src/repository/index.rs
Original file line number Diff line number Diff line change
@@ -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<worktree::Index, worktree::open_index::Error> {
self.try_index().and_then(|opt| match opt {
22 changes: 14 additions & 8 deletions gix/src/revision/spec/parse/delegate/navigate.rs
Original file line number Diff line number Diff line change
@@ -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,
});
2 changes: 1 addition & 1 deletion gix/src/revision/spec/parse/types.rs
Original file line number Diff line number Diff line change
@@ -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,
14 changes: 8 additions & 6 deletions gix/src/status/index_worktree.rs
Original file line number Diff line number Diff line change
@@ -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<BString>) -> Result<index_worktree::Iter, Error> {
pub fn into_index_worktree_iter(
self,
patterns: impl IntoIterator<Item = BString>,
) -> Result<index_worktree::Iter, Error> {
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({