#![allow(clippy::result_large_err)]//! Submodule plumbing and abstractions//!use std::{ borrow::Cow, cell::{Ref, RefCell, RefMut}, path::PathBuf,}; pub use gix_submodule::*; use crate::{bstr::BStr, is_dir_to_mode, worktree::IndexPersistedOrInMemory, Repository, Submodule}; pub(crate) type ModulesFileStorage = gix_features::threading::OwnShared<gix_fs::SharedFileSnapshotMut<File>>;/// A lazily loaded and auto-updated worktree index.pub type ModulesSnapshot = gix_fs::SharedFileSnapshot<File>; /// The name of the file containing (sub) module information.pub(crate) const MODULES_FILE: &str = ".gitmodules"; mod errors;pub use errors::*; /// A platform maintaining state needed to interact with submodules, created by [`Repository::submodules()].pub(crate) struct SharedState<'repo> { pub repo: &'repo Repository, pub(crate) modules: ModulesSnapshot, is_active: RefCell<Option<IsActiveState>>, index: RefCell<Option<IndexPersistedOrInMemory>>,} impl<'repo> SharedState<'repo> { pub(crate) fn new(repo: &'repo Repository, modules: ModulesSnapshot) -> Self { SharedState { repo, modules, is_active: RefCell::new(None), index: RefCell::new(None), } } fn index(&self) -> Result<Ref<'_, IndexPersistedOrInMemory>, crate::repository::index_or_load_from_head::Error> { { let mut state = self.index.borrow_mut(); if state.is_none() { *state = self.repo.index_or_load_from_head()?.into(); } } Ok(Ref::map(self.index.borrow(), |opt| { opt.as_ref().expect("just initialized") })) } fn active_state_mut( &self, ) -> Result<(RefMut<'_, IsActivePlatform>, RefMut<'_, gix_worktree::Stack>), is_active::Error> { let mut state = self.is_active.borrow_mut(); if state.is_none() { let platform = self .modules .is_active_platform(&self.repo.config.resolved, self.repo.config.pathspec_defaults()?)?; let index = self.index()?; let attributes = self .repo .attributes_only( &index, gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping .adjust_for_bare(self.repo.is_bare()), )? .detach(); *state = Some(IsActiveState { platform, attributes }); } Ok(RefMut::map_split(state, |opt| { let state = opt.as_mut().expect("populated above"); (&mut state.platform, &mut state.attributes) })) }} struct IsActiveState { platform: IsActivePlatform, attributes: gix_worktree::Stack,} ///Accessimpl Submodule<'_> { /// Return the submodule's name. pub fn name(&self) -> &BStr { self.name.as_ref() } /// Return the path at which the submodule can be found, relative to the repository. /// /// For details, see [gix_submodule::File::path()]. pub fn path(&self) -> Result<Cow<'_, BStr>, config::path::Error> { self.state.modules.path(self.name()) } /// Return the url from which to clone or update the submodule. /// /// This method takes into consideration submodule configuration overrides. pub fn url(&self) -> Result<gix_url::Url, config::url::Error> { self.state.modules.url(self.name()) } /// Return the `update` field from this submodule's configuration, if present, or `None`. /// /// This method takes into consideration submodule configuration overrides. pub fn update(&self) -> Result<Option<config::Update>, config::update::Error> { self.state.modules.update(self.name()) } /// Return the `branch` field from this submodule's configuration, if present, or `None`. /// /// This method takes into consideration submodule configuration overrides. pub fn branch(&self) -> Result<Option<config::Branch>, config::branch::Error> { self.state.modules.branch(self.name()) } /// Return the `fetchRecurseSubmodules` field from this submodule's configuration, or retrieve the value from `fetch.recurseSubmodules` if unset. pub fn fetch_recurse(&self) -> Result<Option<config::FetchRecurse>, fetch_recurse::Error> { Ok(match self.state.modules.fetch_recurse(self.name())? { Some(val) => Some(val), None => self .state .repo .config .resolved .boolean("fetch.recurseSubmodules") .map(|res| crate::config::tree::Fetch::RECURSE_SUBMODULES.try_into_recurse_submodules(res)) .transpose()?, }) } /// Return the `ignore` field from this submodule's configuration, if present, or `None`. /// /// This method takes into consideration submodule configuration overrides. pub fn ignore(&self) -> Result<Option<config::Ignore>, config::Error> { self.state.modules.ignore(self.name()) } /// Return the `shallow` field from this submodule's configuration, if present, or `None`. /// /// If `true`, the submodule will be checked out with `depth = 1`. If unset, `false` is assumed. pub fn shallow(&self) -> Result<Option<bool>, gix_config::value::Error> { self.state.modules.shallow(self.name()) } /// Returns true if this submodule is considered active and can thus participate in an operation. /// /// Please see the [plumbing crate documentation](gix_submodule::IsActivePlatform::is_active()) for details. pub fn is_active(&self) -> Result<bool, is_active::Error> { let (mut platform, mut attributes) = self.state.active_state_mut()?; let is_active = platform.is_active(&self.state.repo.config.resolved, self.name.as_ref(), { &mut |relative_path, case, is_dir, out| { attributes .set_case(case) .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &self.state.repo.objects) .is_ok_and(|platform| platform.matching_attributes(out)) } })?; Ok(is_active) } /// Return the object id of the submodule as stored in the index of the superproject, /// or `None` if it was deleted from the index. /// /// If `None`, but `Some()` when calling [`Self::head_id()`], then the submodule was just deleted but the change /// wasn't yet committed. Note that `None` is also returned if the entry at the submodule path isn't a submodule. /// If `Some()`, but `None` when calling [`Self::head_id()`], then the submodule was just added without having committed the change. pub fn index_id(&self) -> Result<Option<gix_hash::ObjectId>, index_id::Error> { let path = self.path()?; Ok(self .state .index()? .entry_by_path(&path) .and_then(|entry| (entry.mode == gix_index::entry::Mode::COMMIT).then_some(entry.id))) } /// Return the object id of the submodule as stored in `HEAD^{tree}` of the superproject, or `None` if it wasn't yet committed. /// /// If `Some()`, but `None` when calling [`Self::index_id()`], then the submodule was just deleted but the change /// wasn't yet committed. Note that `None` is also returned if the entry at the submodule path isn't a submodule. /// If `None`, but `Some()` when calling [`Self::index_id()`], then the submodule was just added without having committed the change. pub fn head_id(&self) -> Result<Option<gix_hash::ObjectId>, head_id::Error> { let path = self.path()?; Ok(self .state .repo .head_commit()? .tree()? .peel_to_entry_by_path(gix_path::from_bstr(path.as_ref()))? .and_then(|entry| (entry.mode().is_commit()).then_some(entry.inner.oid))) } /// Return the path at which the repository of the submodule should be located. /// /// The directory might not exist yet. pub fn git_dir(&self) -> PathBuf { self.state .repo .common_dir() .join("modules") .join(gix_path::from_bstr(self.name())) } /// Return the path to the location at which the workdir would be checked out. /// /// Note that it may be a path relative to the repository if, for some reason, the parent directory /// doesn't have a working dir set. pub fn work_dir(&self) -> Result<PathBuf, config::path::Error> { let worktree_git = gix_path::from_bstr(self.path()?); Ok(match self.state.repo.workdir() { None => worktree_git.into_owned(), Some(prefix) => prefix.join(worktree_git), }) } /// Return the path at which the repository of the submodule should be located, or the path inside of /// the superproject's worktree where it actually *is* located if the submodule in the 'old-form', thus is a directory /// inside of the superproject's work-tree. /// /// Note that 'old-form' paths returned aren't verified, i.e. the `.git` repository might be corrupt or otherwise /// invalid - it's left to the caller to try to open it. /// /// Also note that the returned path may not actually exist. pub fn git_dir_try_old_form(&self) -> Result<PathBuf, config::path::Error> { let worktree_gitdir_or_modules_gitdir = if self.worktree_gitdir()?.is_dir() { self.worktree_gitdir()? } else { self.git_dir() }; Ok(worktree_gitdir_or_modules_gitdir) } /// Query various parts of the submodule and assemble it into state information. #[doc(alias = "status", alias = "git2")] pub fn state(&self) -> Result<State, config::path::Error> { let maybe_old_path = self.git_dir_try_old_form()?; let git_dir = self.git_dir(); let worktree_git = self.worktree_gitdir()?; let superproject_configuration = self .state .repo .config .resolved .sections_by_name("submodule") .into_iter() .flatten() .any(|section| section.header().subsection_name() == Some(self.name.as_ref())); Ok(State { repository_exists: maybe_old_path.is_dir(), is_old_form: maybe_old_path != git_dir, worktree_checkout: worktree_git.exists(), superproject_configuration, }) } /// Open the submodule as repository, or `None` if the submodule wasn't initialized yet. /// /// More states can be derived here: /// /// * *initialized* - a repository exists, i.e. `Some(repo)` and the working tree is present. /// * *uninitialized* - a repository does not exist, i.e. `None` /// * *deinitialized* - a repository does exist, i.e. `Some(repo)`, but its working tree is empty. /// /// Also see the [state()](Self::state()) method for learning about the submodule. /// The repository can also be used to learn about the submodule `HEAD`, i.e. where its working tree is at, /// which may differ compared to the superproject's index or `HEAD` commit. pub fn open(&self) -> Result<Option<Repository>, open::Error> { match crate::open_opts(self.git_dir_try_old_form()?, self.state.repo.options.clone()) { Ok(mut repo) => { if repo.workdir().is_none() { let wd = self.work_dir()?; // We should always have a workdir, as bare submodules don't exist. // However, it's possible for no workdir to be accessible if there is a symlink in the way. // Just setting it by hand fixes this issue effectively, even though the question remains // if this should work automatically. // For now, let's *not* use the `self.worktree_git()` directory which has its own edge-cases, // while the current solution yields the cleanest paths (i.e. it keeps relative ones). repo.set_workdir(Some(wd))?; } Ok(Some(repo)) } Err(crate::open::Error::NotARepository { .. }) => Ok(None), Err(err) => Err(err.into()), } } fn worktree_gitdir(&self) -> Result<PathBuf, config::path::Error> { Ok(self.work_dir()?.join(gix_discover::DOT_GIT_DIR)) }} ///#[cfg(feature = "status")]pub mod status { use gix_submodule::config; use super::{head_id, index_id, open, Status}; use crate::Submodule; /// The error returned by [Submodule::status()]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error(transparent)] State(#[from] config::path::Error), #[error(transparent)] HeadId(#[from] head_id::Error), #[error(transparent)] IndexId(#[from] index_id::Error), #[error(transparent)] OpenRepository(#[from] open::Error), #[error(transparent)] IgnoreConfiguration(#[from] config::Error), #[error(transparent)] StatusPlatform(#[from] crate::status::Error), #[error(transparent)] StatusIter(#[from] crate::status::into_iter::Error), #[error(transparent)] NextStatusItem(#[from] crate::status::iter::Error), } impl Submodule<'_> { /// Return the status of the submodule. /// /// Use `ignore` to control the portion of the submodule status to ignore. It can be obtained from /// submodule configuration using the [`ignore()`](Submodule::ignore()) method. /// If `check_dirty` is `true`, the computation will stop once the first in a ladder operations /// ordered from cheap to expensive shows that the submodule is dirty. /// Thus, submodules that are clean will still impose the complete set of computation, as given. #[doc(alias = "submodule_status", alias = "git2")] pub fn status( &self, ignore: config::Ignore, check_dirty: bool, ) -> Result<crate::submodule::status::types::Status, Error> { self.status_opts(ignore, check_dirty, &mut |s| s) } /// Return the status of the submodule, just like [`status`](Self::status), but allows to adjust options /// for more control over how the status is performed. /// /// If `check_dirty` is `true`, the computation will stop once the first in a ladder operations /// ordered from cheap to expensive shows that the submodule is dirty. When checking for detailed /// status information (i.e. untracked file, modifications, HEAD-index changes) only the first change /// will be kept to stop as early as possible. /// /// Use `&mut std::convert::identity` for `adjust_options` if no specific options are desired. /// A reason to change them might be to enable sorting to enjoy deterministic order of changes. /// /// The status allows to easily determine if a submodule [has changes](Status::is_dirty). #[doc(alias = "submodule_status", alias = "git2")] pub fn status_opts( &self, ignore: config::Ignore, check_dirty: bool, adjust_options: &mut dyn for<'a> FnMut( crate::status::Platform<'a, gix_features::progress::Discard>, ) -> crate::status::Platform<'a, gix_features::progress::Discard>, ) -> Result<Status, Error> { let mut state = self.state()?; if ignore == config::Ignore::All { return Ok(Status { state, ..Default::default() }); } let index_id = self.index_id()?; if !state.repository_exists { return Ok(Status { state, index_id, ..Default::default() }); } let sm_repo = match self.open()? { None => { state.repository_exists = false; return Ok(Status { state, index_id, ..Default::default() }); } Some(repo) => repo, }; let checked_out_head_id = sm_repo.head_id().ok().map(crate::Id::detach); let mut status = Status { state, index_id, checked_out_head_id, ..Default::default() }; if ignore == config::Ignore::Dirty || check_dirty && status.is_dirty() == Some(true) { return Ok(status); } if !state.worktree_checkout { return Ok(status); } let statuses = adjust_options(sm_repo.status(gix_features::progress::Discard)?) .index_worktree_options_mut(|opts| { if ignore == config::Ignore::Untracked { opts.dirwalk_options = None; } }) .into_iter(None)?; let mut changes = Vec::new(); for change in statuses { changes.push(change?); if check_dirty { break; } } status.changes = Some(changes); Ok(status) } } impl Status { /// Return `Some(true)` if the submodule status could be determined sufficiently and /// if there are changes that would render this submodule dirty. /// /// Return `Some(false)` if the submodule status could be determined and it has no changes /// at all. /// /// Return `None` if the repository clone or the worktree are missing entirely, which would leave /// it to the caller to determine if that's considered dirty or not. pub fn is_dirty(&self) -> Option<bool> { if !self.state.worktree_checkout || !self.state.repository_exists { return None; } let is_dirty = self.checked_out_head_id != self.index_id || self.changes.as_ref().is_some_and(|c| !c.is_empty()); Some(is_dirty) } } pub(super) mod types { use crate::submodule::State; /// A simplified status of the Submodule. /// /// As opposed to the similar-sounding [`State`], it is more exhaustive and potentially expensive to compute, /// particularly for submodules without changes. /// /// It's produced by [Submodule::status()](crate::Submodule::status()). #[derive(Default, Clone, PartialEq, Debug)] pub struct Status { /// The cheapest part of the status that is always performed, to learn if the repository is cloned /// and if there is a worktree checkout. pub state: State, /// The commit at which the submodule is supposed to be according to the super-project's index. /// `None` means the computation wasn't performed, or the submodule didn't exist in the super-project's index anymore. pub index_id: Option<gix_hash::ObjectId>, /// The commit-id of the `HEAD` at which the submodule is currently checked out. /// `None` if the computation wasn't performed as it was skipped early, or if no repository was available or /// if the HEAD could not be obtained or wasn't born. pub checked_out_head_id: Option<gix_hash::ObjectId>, /// The set of changes obtained from running something akin to `git status` in the submodule working tree. /// /// `None` if the computation wasn't performed as the computation was skipped early, or if no working tree was /// available or repository was available. pub changes: Option<Vec<crate::status::Item>>, } }}#[cfg(feature = "status")]pub use status::types::Status; /// A summary of the state of all parts forming a submodule, which allows to answer various questions about it.////// Note that expensive questions about its presence in the `HEAD` or the `index` are left to the caller.#[derive(Default, Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]pub struct State { /// if the submodule repository has been cloned. pub repository_exists: bool, /// if the submodule repository is located directly in the worktree of the superproject. pub is_old_form: bool, /// if the worktree is checked out. pub worktree_checkout: bool, /// If submodule configuration was found in the superproject's `.git/config` file. /// Note that the presence of a single section is enough, independently of the actual values. pub superproject_configuration: bool,}
#![allow(clippy::result_large_err)]
//! Submodule plumbing and abstractions
//!
use std::{
borrow::Cow,
cell::{Ref, RefCell, RefMut},
path::PathBuf,
};
pub use gix_submodule::*;
use crate::{bstr::BStr, is_dir_to_mode, worktree::IndexPersistedOrInMemory, Repository, Submodule};
pub(crate) type ModulesFileStorage = gix_features::threading::OwnShared<gix_fs::SharedFileSnapshotMut<File>>;
/// A lazily loaded and auto-updated worktree index.
pub type ModulesSnapshot = gix_fs::SharedFileSnapshot<File>;
/// The name of the file containing (sub) module information.
pub(crate) const MODULES_FILE: &str = ".gitmodules";
mod errors;
pub use errors::*;
/// A platform maintaining state needed to interact with submodules, created by [`Repository::submodules()].
pub(crate) struct SharedState<'repo> {
pub repo: &'repo Repository,
pub(crate) modules: ModulesSnapshot,
is_active: RefCell<Option<IsActiveState>>,
index: RefCell<Option<IndexPersistedOrInMemory>>,
}
impl<'repo> SharedState<'repo> {
pub(crate) fn new(repo: &'repo Repository, modules: ModulesSnapshot) -> Self {
SharedState {
repo,
modules,
is_active: RefCell::new(None),
index: RefCell::new(None),
}
}
fn index(&self) -> Result<Ref<'_, IndexPersistedOrInMemory>, crate::repository::index_or_load_from_head::Error> {
{
let mut state = self.index.borrow_mut();
if state.is_none() {
*state = self.repo.index_or_load_from_head()?.into();
}
}
Ok(Ref::map(self.index.borrow(), |opt| {
opt.as_ref().expect("just initialized")
}))
}
fn active_state_mut(
&self,
) -> Result<(RefMut<'_, IsActivePlatform>, RefMut<'_, gix_worktree::Stack>), is_active::Error> {
let mut state = self.is_active.borrow_mut();
if state.is_none() {
let platform = self
.modules
.is_active_platform(&self.repo.config.resolved, self.repo.config.pathspec_defaults()?)?;
let index = self.index()?;
let attributes = self
.repo
.attributes_only(
&index,
gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping
.adjust_for_bare(self.repo.is_bare()),
)?
.detach();
*state = Some(IsActiveState { platform, attributes });
}
Ok(RefMut::map_split(state, |opt| {
let state = opt.as_mut().expect("populated above");
(&mut state.platform, &mut state.attributes)
}))
}
}
struct IsActiveState {
platform: IsActivePlatform,
attributes: gix_worktree::Stack,
}
///Access
impl Submodule<'_> {
/// Return the submodule's name.
pub fn name(&self) -> &BStr {
self.name.as_ref()
}
/// Return the path at which the submodule can be found, relative to the repository.
///
/// For details, see [gix_submodule::File::path()].
pub fn path(&self) -> Result<Cow<'_, BStr>, config::path::Error> {
self.state.modules.path(self.name())
}
/// Return the url from which to clone or update the submodule.
///
/// This method takes into consideration submodule configuration overrides.
pub fn url(&self) -> Result<gix_url::Url, config::url::Error> {
self.state.modules.url(self.name())
}
/// Return the `update` field from this submodule's configuration, if present, or `None`.
///
/// This method takes into consideration submodule configuration overrides.
pub fn update(&self) -> Result<Option<config::Update>, config::update::Error> {
self.state.modules.update(self.name())
}
/// Return the `branch` field from this submodule's configuration, if present, or `None`.
///
/// This method takes into consideration submodule configuration overrides.
pub fn branch(&self) -> Result<Option<config::Branch>, config::branch::Error> {
self.state.modules.branch(self.name())
}
/// Return the `fetchRecurseSubmodules` field from this submodule's configuration, or retrieve the value from `fetch.recurseSubmodules` if unset.
pub fn fetch_recurse(&self) -> Result<Option<config::FetchRecurse>, fetch_recurse::Error> {
Ok(match self.state.modules.fetch_recurse(self.name())? {
Some(val) => Some(val),
None => self
.state
.repo
.config
.resolved
.boolean("fetch.recurseSubmodules")
.map(|res| crate::config::tree::Fetch::RECURSE_SUBMODULES.try_into_recurse_submodules(res))
.transpose()?,
})
}
/// Return the `ignore` field from this submodule's configuration, if present, or `None`.
///
/// This method takes into consideration submodule configuration overrides.
pub fn ignore(&self) -> Result<Option<config::Ignore>, config::Error> {
self.state.modules.ignore(self.name())
}
/// Return the `shallow` field from this submodule's configuration, if present, or `None`.
///
/// If `true`, the submodule will be checked out with `depth = 1`. If unset, `false` is assumed.
pub fn shallow(&self) -> Result<Option<bool>, gix_config::value::Error> {
self.state.modules.shallow(self.name())
}
/// Returns true if this submodule is considered active and can thus participate in an operation.
///
/// Please see the [plumbing crate documentation](gix_submodule::IsActivePlatform::is_active()) for details.
pub fn is_active(&self) -> Result<bool, is_active::Error> {
let (mut platform, mut attributes) = self.state.active_state_mut()?;
let is_active = platform.is_active(&self.state.repo.config.resolved, self.name.as_ref(), {
&mut |relative_path, case, is_dir, out| {
attributes
.set_case(case)
.at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &self.state.repo.objects)
.is_ok_and(|platform| platform.matching_attributes(out))
}
})?;
Ok(is_active)
}
/// Return the object id of the submodule as stored in the index of the superproject,
/// or `None` if it was deleted from the index.
///
/// If `None`, but `Some()` when calling [`Self::head_id()`], then the submodule was just deleted but the change
/// wasn't yet committed. Note that `None` is also returned if the entry at the submodule path isn't a submodule.
/// If `Some()`, but `None` when calling [`Self::head_id()`], then the submodule was just added without having committed the change.
pub fn index_id(&self) -> Result<Option<gix_hash::ObjectId>, index_id::Error> {
let path = self.path()?;
Ok(self
.state
.index()?
.entry_by_path(&path)
.and_then(|entry| (entry.mode == gix_index::entry::Mode::COMMIT).then_some(entry.id)))
}
/// Return the object id of the submodule as stored in `HEAD^{tree}` of the superproject, or `None` if it wasn't yet committed.
///
/// If `Some()`, but `None` when calling [`Self::index_id()`], then the submodule was just deleted but the change
/// wasn't yet committed. Note that `None` is also returned if the entry at the submodule path isn't a submodule.
/// If `None`, but `Some()` when calling [`Self::index_id()`], then the submodule was just added without having committed the change.
pub fn head_id(&self) -> Result<Option<gix_hash::ObjectId>, head_id::Error> {
let path = self.path()?;
Ok(self
.state
.repo
.head_commit()?
.tree()?
.peel_to_entry_by_path(gix_path::from_bstr(path.as_ref()))?
.and_then(|entry| (entry.mode().is_commit()).then_some(entry.inner.oid)))
}
/// Return the path at which the repository of the submodule should be located.
///
/// The directory might not exist yet.
pub fn git_dir(&self) -> PathBuf {
self.state
.repo
.common_dir()
.join("modules")
.join(gix_path::from_bstr(self.name()))
}
/// Return the path to the location at which the workdir would be checked out.
///
/// Note that it may be a path relative to the repository if, for some reason, the parent directory
/// doesn't have a working dir set.
pub fn work_dir(&self) -> Result<PathBuf, config::path::Error> {
let worktree_git = gix_path::from_bstr(self.path()?);
Ok(match self.state.repo.workdir() {
None => worktree_git.into_owned(),
Some(prefix) => prefix.join(worktree_git),
})
}
/// Return the path at which the repository of the submodule should be located, or the path inside of
/// the superproject's worktree where it actually *is* located if the submodule in the 'old-form', thus is a directory
/// inside of the superproject's work-tree.
///
/// Note that 'old-form' paths returned aren't verified, i.e. the `.git` repository might be corrupt or otherwise
/// invalid - it's left to the caller to try to open it.
///
/// Also note that the returned path may not actually exist.
pub fn git_dir_try_old_form(&self) -> Result<PathBuf, config::path::Error> {
let worktree_gitdir_or_modules_gitdir = if self.worktree_gitdir()?.is_dir() {
self.worktree_gitdir()?
} else {
self.git_dir()
};
Ok(worktree_gitdir_or_modules_gitdir)
}
/// Query various parts of the submodule and assemble it into state information.
#[doc(alias = "status", alias = "git2")]
pub fn state(&self) -> Result<State, config::path::Error> {
let maybe_old_path = self.git_dir_try_old_form()?;
let git_dir = self.git_dir();
let worktree_git = self.worktree_gitdir()?;
let superproject_configuration = self
.state
.repo
.config
.resolved
.sections_by_name("submodule")
.into_iter()
.flatten()
.any(|section| section.header().subsection_name() == Some(self.name.as_ref()));
Ok(State {
repository_exists: maybe_old_path.is_dir(),
is_old_form: maybe_old_path != git_dir,
worktree_checkout: worktree_git.exists(),
superproject_configuration,
})
}
/// Open the submodule as repository, or `None` if the submodule wasn't initialized yet.
///
/// More states can be derived here:
///
/// * *initialized* - a repository exists, i.e. `Some(repo)` and the working tree is present.
/// * *uninitialized* - a repository does not exist, i.e. `None`
/// * *deinitialized* - a repository does exist, i.e. `Some(repo)`, but its working tree is empty.
///
/// Also see the [state()](Self::state()) method for learning about the submodule.
/// The repository can also be used to learn about the submodule `HEAD`, i.e. where its working tree is at,
/// which may differ compared to the superproject's index or `HEAD` commit.
pub fn open(&self) -> Result<Option<Repository>, open::Error> {
match crate::open_opts(self.git_dir_try_old_form()?, self.state.repo.options.clone()) {
Ok(mut repo) => {
if repo.workdir().is_none() {
let wd = self.work_dir()?;
// We should always have a workdir, as bare submodules don't exist.
// However, it's possible for no workdir to be accessible if there is a symlink in the way.
// Just setting it by hand fixes this issue effectively, even though the question remains
// if this should work automatically.
// For now, let's *not* use the `self.worktree_git()` directory which has its own edge-cases,
// while the current solution yields the cleanest paths (i.e. it keeps relative ones).
repo.set_workdir(Some(wd))?;
}
Ok(Some(repo))
}
Err(crate::open::Error::NotARepository { .. }) => Ok(None),
Err(err) => Err(err.into()),
}
}
fn worktree_gitdir(&self) -> Result<PathBuf, config::path::Error> {
Ok(self.work_dir()?.join(gix_discover::DOT_GIT_DIR))
}
}
///
#[cfg(feature = "status")]
pub mod status {
use gix_submodule::config;
use super::{head_id, index_id, open, Status};
use crate::Submodule;
/// The error returned by [Submodule::status()].
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error(transparent)]
State(#[from] config::path::Error),
#[error(transparent)]
HeadId(#[from] head_id::Error),
#[error(transparent)]
IndexId(#[from] index_id::Error),
#[error(transparent)]
OpenRepository(#[from] open::Error),
#[error(transparent)]
IgnoreConfiguration(#[from] config::Error),
#[error(transparent)]
StatusPlatform(#[from] crate::status::Error),
#[error(transparent)]
StatusIter(#[from] crate::status::into_iter::Error),
#[error(transparent)]
NextStatusItem(#[from] crate::status::iter::Error),
}
impl Submodule<'_> {
/// Return the status of the submodule.
///
/// Use `ignore` to control the portion of the submodule status to ignore. It can be obtained from
/// submodule configuration using the [`ignore()`](Submodule::ignore()) method.
/// If `check_dirty` is `true`, the computation will stop once the first in a ladder operations
/// ordered from cheap to expensive shows that the submodule is dirty.
/// Thus, submodules that are clean will still impose the complete set of computation, as given.
#[doc(alias = "submodule_status", alias = "git2")]
pub fn status(
&self,
ignore: config::Ignore,
check_dirty: bool,
) -> Result<crate::submodule::status::types::Status, Error> {
self.status_opts(ignore, check_dirty, &mut |s| s)
}
/// Return the status of the submodule, just like [`status`](Self::status), but allows to adjust options
/// for more control over how the status is performed.
///
/// If `check_dirty` is `true`, the computation will stop once the first in a ladder operations
/// ordered from cheap to expensive shows that the submodule is dirty. When checking for detailed
/// status information (i.e. untracked file, modifications, HEAD-index changes) only the first change
/// will be kept to stop as early as possible.
///
/// Use `&mut std::convert::identity` for `adjust_options` if no specific options are desired.
/// A reason to change them might be to enable sorting to enjoy deterministic order of changes.
///
/// The status allows to easily determine if a submodule [has changes](Status::is_dirty).
#[doc(alias = "submodule_status", alias = "git2")]
pub fn status_opts(
&self,
ignore: config::Ignore,
check_dirty: bool,
adjust_options: &mut dyn for<'a> FnMut(
crate::status::Platform<'a, gix_features::progress::Discard>,
)
-> crate::status::Platform<'a, gix_features::progress::Discard>,
) -> Result<Status, Error> {
let mut state = self.state()?;
if ignore == config::Ignore::All {
return Ok(Status {
state,
..Default::default()
});
}
let index_id = self.index_id()?;
if !state.repository_exists {
return Ok(Status {
state,
index_id,
..Default::default()
});
}
let sm_repo = match self.open()? {
None => {
state.repository_exists = false;
return Ok(Status {
state,
index_id,
..Default::default()
});
}
Some(repo) => repo,
};
let checked_out_head_id = sm_repo.head_id().ok().map(crate::Id::detach);
let mut status = Status {
state,
index_id,
checked_out_head_id,
..Default::default()
};
if ignore == config::Ignore::Dirty || check_dirty && status.is_dirty() == Some(true) {
return Ok(status);
}
if !state.worktree_checkout {
return Ok(status);
}
let statuses = adjust_options(sm_repo.status(gix_features::progress::Discard)?)
.index_worktree_options_mut(|opts| {
if ignore == config::Ignore::Untracked {
opts.dirwalk_options = None;
}
})
.into_iter(None)?;
let mut changes = Vec::new();
for change in statuses {
changes.push(change?);
if check_dirty {
break;
}
}
status.changes = Some(changes);
Ok(status)
}
}
impl Status {
/// Return `Some(true)` if the submodule status could be determined sufficiently and
/// if there are changes that would render this submodule dirty.
///
/// Return `Some(false)` if the submodule status could be determined and it has no changes
/// at all.
///
/// Return `None` if the repository clone or the worktree are missing entirely, which would leave
/// it to the caller to determine if that's considered dirty or not.
pub fn is_dirty(&self) -> Option<bool> {
if !self.state.worktree_checkout || !self.state.repository_exists {
return None;
}
let is_dirty =
self.checked_out_head_id != self.index_id || self.changes.as_ref().is_some_and(|c| !c.is_empty());
Some(is_dirty)
}
}
pub(super) mod types {
use crate::submodule::State;
/// A simplified status of the Submodule.
///
/// As opposed to the similar-sounding [`State`], it is more exhaustive and potentially expensive to compute,
/// particularly for submodules without changes.
///
/// It's produced by [Submodule::status()](crate::Submodule::status()).
#[derive(Default, Clone, PartialEq, Debug)]
pub struct Status {
/// The cheapest part of the status that is always performed, to learn if the repository is cloned
/// and if there is a worktree checkout.
pub state: State,
/// The commit at which the submodule is supposed to be according to the super-project's index.
/// `None` means the computation wasn't performed, or the submodule didn't exist in the super-project's index anymore.
pub index_id: Option<gix_hash::ObjectId>,
/// The commit-id of the `HEAD` at which the submodule is currently checked out.
/// `None` if the computation wasn't performed as it was skipped early, or if no repository was available or
/// if the HEAD could not be obtained or wasn't born.
pub checked_out_head_id: Option<gix_hash::ObjectId>,
/// The set of changes obtained from running something akin to `git status` in the submodule working tree.
///
/// `None` if the computation wasn't performed as the computation was skipped early, or if no working tree was
/// available or repository was available.
pub changes: Option<Vec<crate::status::Item>>,
}
}
}
#[cfg(feature = "status")]
pub use status::types::Status;
/// A summary of the state of all parts forming a submodule, which allows to answer various questions about it.
///
/// Note that expensive questions about its presence in the `HEAD` or the `index` are left to the caller.
#[derive(Default, Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct State {
/// if the submodule repository has been cloned.
pub repository_exists: bool,
/// if the submodule repository is located directly in the worktree of the superproject.
pub is_old_form: bool,
/// if the worktree is checked out.
pub worktree_checkout: bool,
/// If submodule configuration was found in the superproject's `.git/config` file.
/// Note that the presence of a single section is enough, independently of the actual values.
pub superproject_configuration: bool,
}