Skip to content

Commit 53de126

Browse files
committed
feat!: add support for submodule status
Previously, submodules where ignored. Now they are treated correctly as 'directory' which is compared to what's in the worktree. We also simplify blob handling.
1 parent c044919 commit 53de126

File tree

9 files changed

+272
-86
lines changed

9 files changed

+272
-86
lines changed

gitoxide-core/src/repository/status.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use gix::bstr::{BStr, BString};
44
use gix::index::Entry;
55
use gix::prelude::FindExt;
66
use gix::Progress;
7-
use gix_status::index_as_worktree::content::FastEq;
7+
use gix_status::index_as_worktree::traits::FastEq;
88
use gix_status::index_as_worktree::Change;
99

1010
pub enum Submodules {

gix-status/src/index_as_worktree/function.rs

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ use gix_features::parallel::{in_parallel_if, Reduce};
77

88
use crate::{
99
index_as_worktree::{
10-
content,
11-
content::CompareBlobs,
10+
traits,
11+
traits::{CompareBlobs, SubmoduleStatus},
1212
types::{Error, Options},
1313
Change, VisitEntry,
1414
},
@@ -26,20 +26,23 @@ use crate::{
2626
/// like whether a file has conflicts, and files that were added with `git add` are shown as a special
2727
/// changes despite not technically requiring a change to the index since `git add` already added the file to the index.
2828
#[allow(clippy::too_many_arguments)]
29-
pub fn index_as_worktree<'index, T, Find, E>(
29+
pub fn index_as_worktree<'index, T, U, Find, E1, E2>(
3030
index: &'index mut gix_index::State,
3131
worktree: &Path,
32-
collector: &mut impl VisitEntry<'index, ContentChange = T>,
32+
collector: &mut impl VisitEntry<'index, ContentChange = T, SubmoduleStatus = U>,
3333
compare: impl CompareBlobs<Output = T> + Send + Clone,
34+
submodule: impl SubmoduleStatus<Output = U, Error = E2> + Send + Clone,
3435
find: Find,
3536
progress: &mut dyn gix_features::progress::Progress,
3637
pathspec: impl Pathspec + Send + Clone,
3738
options: Options,
3839
) -> Result<(), Error>
3940
where
4041
T: Send,
41-
E: std::error::Error + Send + Sync + 'static,
42-
Find: for<'a> FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E> + Send + Clone,
42+
U: Send,
43+
E1: std::error::Error + Send + Sync + 'static,
44+
E2: std::error::Error + Send + Sync + 'static,
45+
Find: for<'a> FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E1> + Send + Clone,
4346
{
4447
// the order is absolutely critical here we use the old timestamp to detect racy index entries
4548
// (modified at or after the last index update) during the index update we then set those
@@ -78,16 +81,17 @@ where
7881
options,
7982
},
8083
compare,
84+
submodule,
8185
find,
8286
pathspec,
8387
)
8488
}
8589
},
86-
|entries, (state, diff, find, pathspec)| {
90+
|entries, (state, blobdiff, submdule, find, pathspec)| {
8791
entries
8892
.iter_mut()
8993
.filter_map(|entry| {
90-
let res = state.process(entry, diff, find, pathspec);
94+
let res = state.process(entry, blobdiff, submdule, find, pathspec);
9195
count.fetch_add(1, Ordering::Relaxed);
9296
res
9397
})
@@ -109,19 +113,21 @@ struct State<'a, 'b> {
109113
options: &'a Options,
110114
}
111115

112-
type StatusResult<'index, T> = Result<(&'index gix_index::Entry, &'index BStr, Option<Change<T>>, bool), Error>;
116+
type StatusResult<'index, T, U> = Result<(&'index gix_index::Entry, &'index BStr, Option<Change<T, U>>, bool), Error>;
113117

114118
impl<'index> State<'_, 'index> {
115-
fn process<T, Find, E>(
119+
fn process<T, U, Find, E1, E2>(
116120
&mut self,
117121
entry: &'index mut gix_index::Entry,
118122
diff: &mut impl CompareBlobs<Output = T>,
123+
submodule: &mut impl SubmoduleStatus<Output = U, Error = E2>,
119124
find: &mut Find,
120125
pathspec: &mut impl Pathspec,
121-
) -> Option<StatusResult<'index, T>>
126+
) -> Option<StatusResult<'index, T, U>>
122127
where
123-
E: std::error::Error + Send + Sync + 'static,
124-
Find: for<'a> FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E>,
128+
E1: std::error::Error + Send + Sync + 'static,
129+
E2: std::error::Error + Send + Sync + 'static,
130+
Find: for<'a> FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E1>,
125131
{
126132
let conflict = match entry.stage() {
127133
0 => false,
@@ -140,7 +146,7 @@ impl<'index> State<'_, 'index> {
140146
if !pathspec.is_included(path, Some(false)) {
141147
return None;
142148
}
143-
let status = self.compute_status(&mut *entry, path, diff, find);
149+
let status = self.compute_status(&mut *entry, path, diff, submodule, find);
144150
Some(status.map(move |status| (&*entry, path, status, conflict)))
145151
}
146152

@@ -183,18 +189,20 @@ impl<'index> State<'_, 'index> {
183189
/// which is a constant.
184190
///
185191
/// Adapted from [here](https://github.com/Byron/gitoxide/pull/805#discussion_r1164676777).
186-
fn compute_status<T, Find, E>(
192+
fn compute_status<T, U, Find, E1, E2>(
187193
&mut self,
188194
entry: &mut gix_index::Entry,
189-
git_path: &BStr,
195+
rela_path: &BStr,
190196
diff: &mut impl CompareBlobs<Output = T>,
197+
submodule: &mut impl SubmoduleStatus<Output = U, Error = E2>,
191198
find: &mut Find,
192-
) -> Result<Option<Change<T>>, Error>
199+
) -> Result<Option<Change<T, U>>, Error>
193200
where
194-
E: std::error::Error + Send + Sync + 'static,
195-
Find: for<'a> FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E>,
201+
E1: std::error::Error + Send + Sync + 'static,
202+
E2: std::error::Error + Send + Sync + 'static,
203+
Find: for<'a> FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E1>,
196204
{
197-
let worktree_path = gix_path::try_from_bstr(git_path).map_err(|_| Error::IllformedUtf8)?;
205+
let worktree_path = gix_path::try_from_bstr(rela_path).map_err(|_| Error::IllformedUtf8)?;
198206
let worktree_path = match self.path_stack.verified_path(worktree_path.as_ref()) {
199207
Ok(path) => path,
200208
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Some(Change::Removed)),
@@ -206,11 +214,17 @@ impl<'index> State<'_, 'index> {
206214
// if a file turned into a directory it was removed
207215
// the only exception here are submodules which are
208216
// part of the index despite being directories
209-
//
210-
// TODO: submodules:
211-
// if entry.mode.contains(Mode::COMMIT) &&
212-
// resolve_gitlink_ref(ce->name, "HEAD", &sub))
213-
return Ok(Some(Change::Removed));
217+
if entry.mode.is_submodule() {
218+
let status = submodule
219+
.status(entry, rela_path)
220+
.map_err(|err| Error::SubmoduleStatus {
221+
rela_path: rela_path.into(),
222+
source: Box::new(err),
223+
})?;
224+
return Ok(status.map(|status| Change::SubmoduleModification(status)));
225+
} else {
226+
return Ok(Some(Change::Removed));
227+
}
214228
}
215229
Ok(metadata) => metadata,
216230
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Some(Change::Removed)),
@@ -265,7 +279,7 @@ impl<'index> State<'_, 'index> {
265279
id: &entry.id,
266280
find,
267281
};
268-
let content_change = diff.compare_blobs::<Error>(entry, metadata.len() as usize, read_file, read_blob)?;
282+
let content_change = diff.compare_blobs(entry, metadata.len() as usize, read_file, read_blob)?;
269283
// This file is racy clean! Set the size to 0 so we keep detecting this as the file is updated.
270284
if content_change.is_some() && racy_clean {
271285
entry.stat.size = 0;
@@ -288,8 +302,10 @@ struct ReduceChange<'a, 'index, T: VisitEntry<'index>> {
288302
phantom: PhantomData<fn(&'index ())>,
289303
}
290304

291-
impl<'index, T, C: VisitEntry<'index, ContentChange = T>> Reduce for ReduceChange<'_, 'index, C> {
292-
type Input = Vec<StatusResult<'index, T>>;
305+
impl<'index, T, U, C: VisitEntry<'index, ContentChange = T, SubmoduleStatus = U>> Reduce
306+
for ReduceChange<'_, 'index, C>
307+
{
308+
type Input = Vec<StatusResult<'index, T, U>>;
293309

294310
type FeedProduce = ();
295311

@@ -327,7 +343,7 @@ where
327343
find: Find,
328344
}
329345

330-
impl<'a> content::ReadDataOnce<'a, Error> for WorktreeBlob<'a> {
346+
impl<'a> traits::ReadDataOnce<'a> for WorktreeBlob<'a> {
331347
fn read_data(self) -> Result<&'a [u8], Error> {
332348
let res = read::data_to_buf_with_meta(
333349
self.path,
@@ -339,7 +355,7 @@ impl<'a> content::ReadDataOnce<'a, Error> for WorktreeBlob<'a> {
339355
}
340356
}
341357

342-
impl<'a, Find, E> content::ReadDataOnce<'a, Error> for OdbBlob<'a, Find, E>
358+
impl<'a, Find, E> traits::ReadDataOnce<'a> for OdbBlob<'a, Find, E>
343359
where
344360
E: std::error::Error + Send + Sync + 'static,
345361
Find: FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E>,

gix-status/src/index_as_worktree/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ mod types;
44
pub use types::{Change, Error, Options, VisitEntry};
55

66
mod recorder;
7-
pub use recorder::Recorder;
7+
pub use recorder::{Record, Recorder};
88

9-
///
10-
pub mod content;
119
pub(crate) mod function;
10+
///
11+
pub mod traits;

gix-status/src/index_as_worktree/recorder.rs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,46 @@ use gix_index as index;
33

44
use crate::index_as_worktree::{Change, VisitEntry};
55

6+
/// A record of a change.
7+
///
8+
/// It's created either if there is a conflict or a change, or both.
9+
#[derive(Debug)]
10+
pub struct Record<'index, T, U> {
11+
/// The index entry that is changed.
12+
pub entry: &'index index::Entry,
13+
/// The path to the entry.
14+
pub relative_path: &'index BStr,
15+
/// The change itself, or `None` if there is only a conflict.
16+
pub change: Option<Change<T, U>>,
17+
/// information about the conflict itself
18+
pub conflict: bool,
19+
}
20+
621
/// Convenience implementation of [`VisitEntry`] that collects all non-trivial changes into a `Vec`.
722
#[derive(Debug, Default)]
8-
pub struct Recorder<'index, T = ()> {
23+
pub struct Recorder<'index, T = (), U = ()> {
924
/// collected changes, index entries without conflicts or changes are excluded.
10-
pub records: Vec<(&'index BStr, Option<Change<T>>, bool)>,
25+
pub records: Vec<Record<'index, T, U>>,
1126
}
1227

13-
impl<'index, T: Send> VisitEntry<'index> for Recorder<'index, T> {
28+
impl<'index, T: Send, U: Send> VisitEntry<'index> for Recorder<'index, T, U> {
1429
type ContentChange = T;
30+
type SubmoduleStatus = U;
1531

1632
fn visit_entry(
1733
&mut self,
18-
_entry: &'index index::Entry,
34+
entry: &'index index::Entry,
1935
rela_path: &'index BStr,
20-
status: Option<Change<Self::ContentChange>>,
36+
change: Option<Change<Self::ContentChange, Self::SubmoduleStatus>>,
2137
conflict: bool,
2238
) {
23-
if conflict || status.is_some() {
24-
self.records.push((rela_path, status, conflict))
39+
if conflict || change.is_some() {
40+
self.records.push(Record {
41+
entry,
42+
relative_path: rela_path,
43+
change,
44+
conflict,
45+
})
2546
}
2647
}
2748
}

gix-status/src/index_as_worktree/content.rs renamed to gix-status/src/index_as_worktree/traits.rs

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use crate::index_as_worktree::Error;
2+
use bstr::BStr;
13
use gix_hash::ObjectId;
24
use gix_index as index;
35
use index::Entry;
@@ -11,22 +13,33 @@ pub trait CompareBlobs {
1113
/// and allow reading its bytes using `worktree_blob`.
1214
/// If this function returns `None` the `entry` and the `worktree_blob` are assumed to be identical.
1315
/// Use `entry_blob` to obtain the data for the blob referred to by `entry`, allowing comparisons of the data itself.
14-
fn compare_blobs<'a, E>(
16+
fn compare_blobs<'a, 'b>(
1517
&mut self,
16-
entry: &'a gix_index::Entry,
18+
entry: &gix_index::Entry,
1719
worktree_blob_size: usize,
18-
worktree_blob: impl ReadDataOnce<'a, E>,
19-
entry_blob: impl ReadDataOnce<'a, E>,
20-
) -> Result<Option<Self::Output>, E>;
20+
worktree_blob: impl ReadDataOnce<'a>,
21+
entry_blob: impl ReadDataOnce<'b>,
22+
) -> Result<Option<Self::Output>, Error>;
23+
}
24+
25+
/// Determine the status of a submodule, which always indicates that it changed if present.
26+
pub trait SubmoduleStatus {
27+
/// The status result, describing in which way the submodule changed.
28+
type Output;
29+
/// A custom error that may occur while computing the submodule status.
30+
type Error: std::error::Error + Send + Sync + 'static;
31+
32+
/// Compute the status of the submodule at `entry` and `rela_path`, or return `None` if no change was detected.
33+
fn status(&mut self, entry: &gix_index::Entry, rela_path: &BStr) -> Result<Option<Self::Output>, Self::Error>;
2134
}
2235

2336
/// Lazy borrowed access to blob data.
24-
pub trait ReadDataOnce<'a, E> {
37+
pub trait ReadDataOnce<'a> {
2538
/// Returns the contents of this blob.
2639
///
2740
/// This potentially performs IO and other expensive operations
2841
/// and should only be called when necessary.
29-
fn read_data(self) -> Result<&'a [u8], E>;
42+
fn read_data(self) -> Result<&'a [u8], Error>;
3043
}
3144

3245
/// Compares to blobs by comparing their size and oid, and only looks at the file if
@@ -37,13 +50,13 @@ pub struct FastEq;
3750
impl CompareBlobs for FastEq {
3851
type Output = ();
3952

40-
fn compare_blobs<'a, E>(
53+
fn compare_blobs<'a, 'b>(
4154
&mut self,
42-
entry: &'a Entry,
55+
entry: &Entry,
4356
worktree_blob_size: usize,
44-
worktree_blob: impl ReadDataOnce<'a, E>,
45-
_entry_blob: impl ReadDataOnce<'a, E>,
46-
) -> Result<Option<Self::Output>, E> {
57+
worktree_blob: impl ReadDataOnce<'a>,
58+
_entry_blob: impl ReadDataOnce<'b>,
59+
) -> Result<Option<Self::Output>, Error> {
4760
// make sure to account for racily smudged entries here so that they don't always keep
4861
// showing up as modified even after their contents have changed again, to a potentially
4962
// unmodified state. That means that we want to ignore stat.size == 0 for non_empty_blobs.
@@ -66,13 +79,13 @@ pub struct HashEq;
6679
impl CompareBlobs for HashEq {
6780
type Output = ObjectId;
6881

69-
fn compare_blobs<'a, E>(
82+
fn compare_blobs<'a, 'b>(
7083
&mut self,
71-
entry: &'a Entry,
84+
entry: &Entry,
7285
_worktree_blob_size: usize,
73-
worktree_blob: impl ReadDataOnce<'a, E>,
74-
_entry_blob: impl ReadDataOnce<'a, E>,
75-
) -> Result<Option<Self::Output>, E> {
86+
worktree_blob: impl ReadDataOnce<'a>,
87+
_entry_blob: impl ReadDataOnce<'b>,
88+
) -> Result<Option<Self::Output>, Error> {
7689
let blob = worktree_blob.read_data()?;
7790
let file_hash = gix_object::compute_hash(entry.id.kind(), gix_object::Kind::Blob, blob);
7891
Ok((entry.id != file_hash).then_some(file_hash))

0 commit comments

Comments
 (0)