| #![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, 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(crate) 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<'repo> Submodule<'repo> { |
| /// 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_by_key("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), &self.state.repo.objects) |
| .map_or(false, |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.work_dir() { |
| 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_git = self.work_dir()?.join(gix_discover::DOT_GIT_DIR); |
| Ok(if worktree_git.is_dir() { |
| worktree_git |
| } else { |
| self.git_dir() |
| }) |
| } |
| |
| /// 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.work_dir()?.join(gix_discover::DOT_GIT_DIR); |
| 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(repo) => Ok(Some(repo)), |
| Err(crate::open::Error::NotARepository { .. }) => Ok(None), |
| Err(err) => Err(err.into()), |
| } |
| } |
| } |
| |
| /// |
| #[allow(clippy::empty_docs)] |
| #[cfg(feature = "status")] |
| pub mod status { |
| use super::{head_id, index_id, open, Status}; |
| use crate::Submodule; |
| use gix_submodule::config; |
| |
| /// 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)] |
| Status(#[from] crate::status::index_worktree::iter::Error), |
| #[error(transparent)] |
| IndexWorktreeStatus(#[from] crate::status::index_worktree::Error), |
| } |
| |
| impl<'repo> Submodule<'repo> { |
| /// 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. |
| /// |
| /// 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). |
| /// |
| /// ### Incomplete Implementation Warning |
| /// |
| /// Currently, changes between the head and the index aren't computed. |
| // TODO: Run the full status, including tree->index once available. |
| #[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 statusses = adjust_options(sm_repo.status(gix_features::progress::Discard)?) |
| .index_worktree_options_mut(|opts| { |
| if ignore == config::Ignore::Untracked { |
| opts.dirwalk_options = None; |
| } |
| }) |
| .into_index_worktree_iter(Vec::new())?; |
| let mut changes = Vec::new(); |
| for change in statusses { |
| changes.push(change?); |
| } |
| 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().map_or(false, |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::index_worktree::iter::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, |
| } |