Skip to content

Commit cee7fa2

Browse files
committed
feat: Add Submodule::status() method.
That way it's possible to obtain submodule status information, with enough information to implement `git status`-like commands.
1 parent b876ce0 commit cee7fa2

File tree

6 files changed

+360
-5
lines changed

6 files changed

+360
-5
lines changed

gix-submodule/src/config.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ use bstr::{BStr, BString, ByteSlice};
44
#[derive(Default, Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)]
55
pub enum Ignore {
66
/// Submodule changes won't be considered at all, which is the fastest option.
7-
///
8-
/// Note that changes to the submodule hash in the superproject will still be observable.
97
All,
108
/// Ignore any changes to the submodule working tree, only show committed differences between the `HEAD` of the submodule
119
/// compared to the recorded commit in the superproject.

gix/src/status/platform.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ impl<'repo, Progress> Platform<'repo, Progress>
55
where
66
Progress: gix_features::progress::Progress,
77
{
8-
/// Call `cb` on dirwalk options if these are set (which is the default). The directory walk is used to find
9-
/// untracked files or ignored files.
8+
/// Call `cb` on dirwalk options if these are set (which is the default when created through [`Repository::status()`](crate::Repository::status())).
9+
/// The directory walk is used to find untracked files or ignored files.
10+
///
1011
/// `cb` will be able to run builder-methods on the passed dirwalk options.
1112
pub fn dirwalk_options(mut self, cb: impl FnOnce(crate::dirwalk::Options) -> crate::dirwalk::Options) -> Self {
1213
if let Some(opts) = self.index_worktree_options.dirwalk_options.take() {

gix/src/submodule/mod.rs

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,174 @@ impl<'repo> Submodule<'repo> {
275275
}
276276
}
277277

278+
///
279+
#[cfg(all(feature = "status", feature = "parallel"))]
280+
pub mod status {
281+
use super::{head_id, index_id, open, Status};
282+
use crate::Submodule;
283+
use gix_submodule::config;
284+
285+
/// The error returned by [Submodule::status()].
286+
#[derive(Debug, thiserror::Error)]
287+
#[allow(missing_docs)]
288+
pub enum Error {
289+
#[error(transparent)]
290+
State(#[from] config::path::Error),
291+
#[error(transparent)]
292+
HeadId(#[from] head_id::Error),
293+
#[error(transparent)]
294+
IndexId(#[from] index_id::Error),
295+
#[error(transparent)]
296+
OpenRepository(#[from] open::Error),
297+
#[error(transparent)]
298+
IgnoreConfiguration(#[from] config::Error),
299+
#[error(transparent)]
300+
StatusPlatform(#[from] crate::config::boolean::Error),
301+
#[error(transparent)]
302+
Status(#[from] crate::status::index_worktree::iter::Error),
303+
}
304+
305+
impl<'repo> Submodule<'repo> {
306+
/// Return the status of the submodule.
307+
///
308+
/// Use `ignore` to control the portion of the submodule status to ignore. It can be obtained from
309+
/// submodule configuration using the [`ignore()`](Submodule::ignore()) method.
310+
/// If `check_dirty` is `true`, the computation will stop once the first in a ladder operations
311+
/// ordered from cheap to expensive shows that the submodule is dirty.
312+
/// Thus, submodules that are clean will still impose the complete set of computation, as given.
313+
#[doc(alias = "submodule_status", alias = "git2")]
314+
pub fn status(
315+
&self,
316+
ignore: config::Ignore,
317+
check_dirty: bool,
318+
) -> Result<crate::submodule::status::types::Status, Error> {
319+
self.status_opts(ignore, check_dirty, &mut |s| s)
320+
}
321+
/// Return the status of the submodule, just like [`status`](Self::status), but allows to adjust options
322+
/// for more control over how the status is performed.
323+
///
324+
/// Use `&mut std::convert::identity` for `adjust_options` if no specific options are desired.
325+
/// A reason to change them might be to enable sorting to enjoy deterministic order of changes.
326+
///
327+
/// The status allows to easily determine if a submodule [has changes](Status::is_dirty).
328+
#[doc(alias = "submodule_status", alias = "git2")]
329+
pub fn status_opts(
330+
&self,
331+
ignore: config::Ignore,
332+
check_dirty: bool,
333+
adjust_options: &mut dyn for<'a> FnMut(
334+
crate::status::Platform<'a, gix_features::progress::Discard>,
335+
)
336+
-> crate::status::Platform<'a, gix_features::progress::Discard>,
337+
) -> Result<Status, Error> {
338+
let mut state = self.state()?;
339+
if ignore == config::Ignore::All {
340+
return Ok(Status {
341+
state,
342+
..Default::default()
343+
});
344+
}
345+
346+
let index_id = self.index_id()?;
347+
if !state.repository_exists {
348+
return Ok(Status {
349+
state,
350+
index_id,
351+
..Default::default()
352+
});
353+
}
354+
let sm_repo = match self.open()? {
355+
None => {
356+
state.repository_exists = false;
357+
return Ok(Status {
358+
state,
359+
index_id,
360+
..Default::default()
361+
});
362+
}
363+
Some(repo) => repo,
364+
};
365+
366+
let checked_out_head_id = sm_repo.head_id().ok().map(crate::Id::detach);
367+
let mut status = Status {
368+
state,
369+
index_id,
370+
checked_out_head_id,
371+
..Default::default()
372+
};
373+
if ignore == config::Ignore::Dirty || check_dirty && status.is_dirty() == Some(true) {
374+
return Ok(status);
375+
}
376+
377+
status.changes = Some(
378+
adjust_options(sm_repo.status(gix_features::progress::Discard)?)
379+
// TODO: Run the full status, including tree->index once available.
380+
.index_worktree_options_mut(|opts| {
381+
assert!(opts.dirwalk_options.is_some(), "BUG: it's supposed to be the default");
382+
if ignore == config::Ignore::Untracked {
383+
opts.dirwalk_options = None;
384+
}
385+
})
386+
.into_index_worktree_iter(Vec::new())?
387+
.filter_map(Result::ok)
388+
.collect(),
389+
);
390+
391+
Ok(status)
392+
}
393+
}
394+
395+
impl Status {
396+
/// Return `Some(true)` if the submodule status could be determined sufficiently and
397+
/// if there are changes that would render this submodule dirty.
398+
///
399+
/// Return `Some(false)` if the submodule status could be determined and it has no changes
400+
/// at all.
401+
///
402+
/// Return `None` if the repository clone or the worktree are missing entirely, which would leave
403+
/// it to the caller to determine if that's considered dirty or not.
404+
pub fn is_dirty(&self) -> Option<bool> {
405+
if !self.state.worktree_checkout && !self.state.repository_exists {
406+
return None;
407+
}
408+
let is_dirty =
409+
self.checked_out_head_id != self.index_id || self.changes.as_ref().map_or(false, |c| !c.is_empty());
410+
Some(is_dirty)
411+
}
412+
}
413+
414+
pub(super) mod types {
415+
use crate::submodule::State;
416+
417+
/// A simplified status of the Submodule.
418+
///
419+
/// As opposed to the similar-sounding [`State`], it is more exhaustive and potentially expensive to compute,
420+
/// particularly for submodules without changes.
421+
///
422+
/// It's produced by [Submodule::status()](crate::Submodule::status()).
423+
#[derive(Default, Clone, PartialEq, Debug)]
424+
pub struct Status {
425+
/// The cheapest part of the status that is always performed, to learn if the repository is cloned
426+
/// and if there is a worktree checkout.
427+
pub state: State,
428+
/// The commit at which the submodule is supposed to be according to the super-project's index.
429+
/// `None` means the computation wasn't performed, or the submodule didn't exist in the super-project's index anymore.
430+
pub index_id: Option<gix_hash::ObjectId>,
431+
/// The commit-id of the `HEAD` at which the submodule is currently checked out.
432+
/// `None` if the computation wasn't performed as it was skipped early, or if no repository was available or
433+
/// if the HEAD could not be obtained or wasn't born.
434+
pub checked_out_head_id: Option<gix_hash::ObjectId>,
435+
/// The set of changes obtained from running something akin to `git status` in the submodule working tree.
436+
///
437+
/// `None` if the computation wasn't performed as the computation was skipped early, or if no working tree was
438+
/// available or repository was available.
439+
pub changes: Option<Vec<crate::status::index_worktree::iter::Item>>,
440+
}
441+
}
442+
}
443+
#[cfg(all(feature = "status", feature = "parallel"))]
444+
pub use status::types::Status;
445+
278446
/// A summary of the state of all parts forming a submodule, which allows to answer various questions about it.
279447
///
280448
/// Note that expensive questions about its presence in the `HEAD` or the `index` are left to the caller.
Binary file not shown.

gix/tests/fixtures/make_submodules.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,36 @@ git init -q module1
1212
git commit -q -am c2
1313
)
1414

15+
git init submodule-head-changed
16+
(cd submodule-head-changed
17+
git submodule add ../module1 m1
18+
git commit -m "add submodule"
19+
20+
cd m1 && git checkout @~1
21+
)
22+
23+
git init modified-and-untracked
24+
(cd modified-and-untracked
25+
git submodule add ../module1 m1
26+
git commit -m "add submodule"
27+
28+
(cd m1
29+
echo change >> this
30+
touch new
31+
)
32+
)
33+
34+
git init submodule-head-changed-and-modified
35+
(cd submodule-head-changed-and-modified
36+
git submodule add ../module1 m1
37+
git commit -m "add submodule"
38+
39+
(cd m1
40+
git checkout @~1
41+
echo change >> this
42+
)
43+
)
44+
1545
git init with-submodules
1646
(cd with-submodules
1747
mkdir dir

0 commit comments

Comments
 (0)