Skip to content

Commit d5ec06b

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 594c838 commit d5ec06b

File tree

6 files changed

+391
-5
lines changed

6 files changed

+391
-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: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,182 @@ 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+
/// How to obtain a submodule's status.
286+
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
287+
pub enum Mode {
288+
/// Use the ['ignore' value](crate::Submodule::ignore) to determine which submodules
289+
/// participate in the status query, and to which extent.
290+
AsConfigured {
291+
/// If `true`, default `false`, the computation will stop once the first in a ladder operations
292+
/// ordered from cheap to expensive shows that the submodule is dirty.
293+
/// Thus, submodules that are clean will still impose the complete set of computation, as configured.
294+
check_dirty: bool,
295+
},
296+
/// Instead of the configuration, use the given ['ignore' value](crate::submodule::config::Ignore).
297+
/// This makes it possible to fine-tune the amount of work invested in this status, while allowing
298+
/// to turn off all submodule status information.
299+
Given {
300+
/// The portion of the submodule status to ignore.
301+
ignore: crate::submodule::config::Ignore,
302+
/// If `true`, default `false`, the computation will stop once the first in a ladder operations
303+
/// ordered from cheap to expensive shows that the submodule is dirty.
304+
/// Thus, submodules that are clean will still impose the complete set of computation, as given.
305+
check_dirty: bool,
306+
},
307+
}
308+
309+
impl Default for Mode {
310+
fn default() -> Self {
311+
Mode::AsConfigured { check_dirty: false }
312+
}
313+
}
314+
315+
/// The error returned by [Submodule::status()].
316+
#[derive(Debug, thiserror::Error)]
317+
#[allow(missing_docs)]
318+
pub enum Error {
319+
#[error(transparent)]
320+
State(#[from] config::path::Error),
321+
#[error(transparent)]
322+
HeadId(#[from] head_id::Error),
323+
#[error(transparent)]
324+
IndexId(#[from] index_id::Error),
325+
#[error(transparent)]
326+
OpenRepository(#[from] open::Error),
327+
#[error(transparent)]
328+
IgnoreConfiguration(#[from] config::Error),
329+
#[error(transparent)]
330+
StatusPlatform(#[from] crate::config::boolean::Error),
331+
#[error(transparent)]
332+
Status(#[from] crate::status::index_worktree::iter::Error),
333+
}
334+
335+
impl<'repo> Submodule<'repo> {
336+
/// Return the status of the submodule based on the `mode`.
337+
///
338+
/// The status allows to easily determine if a submodule [has changes](Status::is_dirty).
339+
#[doc(alias = "submodule_status", alias = "git2")]
340+
pub fn status(&self, mode: Mode) -> Result<Status, Error> {
341+
let mut state = self.state()?;
342+
let (ignore, check_dirty) = match mode {
343+
Mode::Given { ignore, check_dirty } => (ignore, check_dirty),
344+
Mode::AsConfigured { check_dirty } => (self.ignore()?.unwrap_or_default(), check_dirty),
345+
};
346+
if ignore == config::Ignore::All {
347+
return Ok(Status {
348+
state,
349+
..Default::default()
350+
});
351+
}
352+
353+
let index_id = self.index_id()?;
354+
if !state.repository_exists {
355+
return Ok(Status {
356+
state,
357+
index_id,
358+
..Default::default()
359+
});
360+
}
361+
let sm_repo = match self.open()? {
362+
None => {
363+
state.repository_exists = false;
364+
return Ok(Status {
365+
state,
366+
index_id,
367+
..Default::default()
368+
});
369+
}
370+
Some(repo) => repo,
371+
};
372+
373+
let checked_out_head_id = sm_repo.head_id().ok().map(crate::Id::detach);
374+
let mut status = Status {
375+
state,
376+
index_id,
377+
checked_out_head_id,
378+
..Default::default()
379+
};
380+
if ignore == config::Ignore::Dirty || check_dirty && status.is_dirty() == Some(true) {
381+
return Ok(status);
382+
}
383+
384+
status.changes = Some(
385+
sm_repo
386+
.status(gix_features::progress::Discard)?
387+
// TODO: Run the full status, including tree->index once available.
388+
.index_worktree_options_mut(|opts| {
389+
assert!(opts.dirwalk_options.is_some(), "BUG: it's supposed to be the default");
390+
if ignore == config::Ignore::Untracked {
391+
opts.dirwalk_options = None;
392+
}
393+
})
394+
.into_index_worktree_iter(Vec::new())?
395+
.filter_map(Result::ok)
396+
.collect(),
397+
);
398+
399+
Ok(status)
400+
}
401+
}
402+
403+
impl Status {
404+
/// Return `Some(true)` if the submodule status could be determined sufficiently and
405+
/// if there are changes that would render this submodule dirty.
406+
///
407+
/// Return `Some(false)` if the submodule status could be determined and it has no changes
408+
/// at all.
409+
///
410+
/// Return `None` if the repository clone or the worktree are missing entirely, which would leave
411+
/// it to the caller to determine if that's considered dirty or not.
412+
pub fn is_dirty(&self) -> Option<bool> {
413+
if !self.state.worktree_checkout && !self.state.repository_exists {
414+
return None;
415+
}
416+
let is_dirty =
417+
self.checked_out_head_id != self.index_id || self.changes.as_ref().map_or(false, |c| !c.is_empty());
418+
Some(is_dirty)
419+
}
420+
}
421+
422+
pub(super) mod types {
423+
use crate::submodule::State;
424+
425+
/// A simplified status of the Submodule.
426+
///
427+
/// As opposed to the similar-sounding [`State`], it is more exhaustive and potentially expensive to compute,
428+
/// particularly for submodules without changes.
429+
///
430+
/// It's produced by [Submodule::status()](crate::Submodule::status()).
431+
#[derive(Default, Clone, PartialEq, Debug)]
432+
pub struct Status {
433+
/// The cheapest part of the status that is always performed, to learn if the repository is cloned
434+
/// and if there is a worktree checkout.
435+
pub state: State,
436+
/// The commit at which the submodule is supposed to be according to the super-project's index.
437+
/// `None` means the computation wasn't performed, or the submodule didn't exist in the super-project's index anymore.
438+
pub index_id: Option<gix_hash::ObjectId>,
439+
/// The commit-id of the `HEAD` at which the submodule is currently checked out.
440+
/// `None` if the computation wasn't performed as it was skipped early, or if no repository was available or
441+
/// if the HEAD could not be obtained or wasn't born.
442+
pub checked_out_head_id: Option<gix_hash::ObjectId>,
443+
/// The set of changes obtained from running something akin to `git status` in the submodule working tree.
444+
///
445+
/// `None` if the computation wasn't performed as the computation was skipped early, or if no working tree was
446+
/// available or repository was available.
447+
pub changes: Option<Vec<crate::status::index_worktree::iter::Item>>,
448+
}
449+
}
450+
}
451+
#[cfg(all(feature = "status", feature = "parallel"))]
452+
pub use status::types::Status;
453+
278454
/// A summary of the state of all parts forming a submodule, which allows to answer various questions about it.
279455
///
280456
/// 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)