#![allow(clippy::result_large_err)]
use std::{
borrow::Cow,
cell::{Ref, RefCell, RefMut},
path::PathBuf,
};
pub use gix_submodule::*;
use crate::{bstr::BStr, repository::IndexPersistedOrInMemory, Repository, Submodule};
pub(crate) type ModulesFileStorage = gix_features::threading::OwnShared<gix_fs::SharedFileSnapshotMut<File>>;
pub type ModulesSnapshot = gix_fs::SharedFileSnapshot<File>;
pub(crate) const MODULES_FILE: &str = ".gitmodules";
mod errors;
pub use errors::*;
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,
}
impl<'repo> Submodule<'repo> {
pub fn name(&self) -> &BStr {
self.name.as_ref()
}
pub fn path(&self) -> Result<Cow<'_, BStr>, config::path::Error> {
self.state.modules.path(self.name())
}
pub fn url(&self) -> Result<gix_url::Url, config::url::Error> {
self.state.modules.url(self.name())
}
pub fn update(&self) -> Result<Option<config::Update>, config::update::Error> {
self.state.modules.update(self.name())
}
pub fn branch(&self) -> Result<Option<config::Branch>, config::branch::Error> {
self.state.modules.branch(self.name())
}
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()?,
})
}
pub fn ignore(&self) -> Result<Option<config::Ignore>, config::Error> {
self.state.modules.ignore(self.name())
}
pub fn shallow(&self) -> Result<Option<bool>, gix_config::value::Error> {
self.state.modules.shallow(self.name())
}
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)
}
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)))
}
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)))
}
pub fn git_dir(&self) -> PathBuf {
self.state
.repo
.common_dir()
.join("modules")
.join(gix_path::from_bstr(self.name()))
}
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),
})
}
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()
})
}
#[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,
})
}
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()),
}
}
}
#[derive(Default, Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct State {
pub repository_exists: bool,
pub is_old_form: bool,
pub worktree_checkout: bool,
pub superproject_configuration: bool,
}