use super::{PopulateReport, UpdateOptions};
use crate::{Repository, workflow};
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct RecursionOptions {
pub update: UpdateOptions,
pub max_depth: usize,
pub max_submodules: usize,
}
impl RecursionOptions {
pub fn new(update: UpdateOptions) -> Self {
Self {
update,
max_depth: 8,
max_submodules: 1024,
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct RecursiveEntry {
pub path: Vec<u8>,
pub initialized: bool,
pub population: Option<PopulateReport>,
}
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct RecursiveReport {
pub entries: Vec<RecursiveEntry>,
}
#[derive(Debug, thiserror::Error)]
#[error(
"recursive submodule population failed at {:?}: {source}",
String::from_utf8_lossy(path)
)]
pub struct RecursiveError {
pub report: Box<RecursiveReport>,
pub path: Vec<u8>,
#[source]
pub source: RecursiveFailure,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum RecursiveFailure {
#[error("recursive submodule population requires Linux/macOS")]
UnsupportedPlatform,
#[error("recursive submodule population requires a checked-out parent")]
NoWorktree,
#[error("cannot populate an unmerged gitlink")]
Unmerged,
#[error("gitlink has no unique submodule definition")]
Definition,
#[error("recursive submodule {0} limit exceeded")]
Limit(&'static str),
#[error("parent index changed during recursive population")]
Changed,
#[error(transparent)]
Prepare(Box<crate::Error>),
#[error(transparent)]
Populate(Box<super::PopulateError>),
}
pub fn populate_recursive(
parent: &Repository,
superproject_url: &[u8],
options: &RecursionOptions,
callbacks: &mut dyn workflow::TransferCallbacks,
) -> Result<RecursiveReport, RecursiveError> {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let mut walker = Walker {
options,
callbacks,
report: RecursiveReport::default(),
path: Vec::new(),
};
match walker.visit(parent, superproject_url, &[], 0) {
Ok(()) => Ok(walker.report),
Err(source) => Err(RecursiveError {
report: Box::new(walker.report),
path: walker.path,
source,
}),
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let _ = (parent, superproject_url, options, callbacks);
Err(RecursiveError {
report: Box::default(),
path: Vec::new(),
source: RecursiveFailure::UnsupportedPlatform,
})
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
struct Walker<'a> {
options: &'a RecursionOptions,
callbacks: &'a mut dyn workflow::TransferCallbacks,
report: RecursiveReport,
path: Vec<u8>,
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl Walker<'_> {
fn visit(
&mut self,
parent: &Repository,
url: &[u8],
prefix: &[u8],
depth: usize,
) -> Result<(), RecursiveFailure> {
use super::{Commands, Modules, Source, Update, UpdateMethod};
self.path = prefix.to_vec();
if parent.worktree().is_none() {
return Err(RecursiveFailure::NoWorktree);
}
let mut parent = parent.reopen().map_err(prepare)?;
let limits = self.options.update.limits.index;
let index = parent.read_index(limits).map_err(prepare)?;
let Some(index) = index else {
return Err(RecursiveFailure::NoWorktree);
};
let links: Vec<_> = index
.entries()
.iter()
.filter(|e| e.mode == crate::index::Mode::Gitlink)
.collect();
if links.is_empty() {
return Ok(());
}
let modules = Modules::read(
&parent,
&parent.objects().map_err(prepare)?,
Source::WorktreeOrIndex,
)
.map_err(prepare)?
.ok_or(RecursiveFailure::Definition)?;
for link in links {
self.path = joined(prefix, &link.path);
let mut matching = modules
.definitions()
.iter()
.filter(|d| d.path() == Some(link.path.as_slice()));
let definition = matching.next().ok_or(RecursiveFailure::Definition)?;
if matching.next().is_some() {
return Err(RecursiveFailure::Definition);
}
if !selected(parent.config(), definition)? {
continue;
}
if link.stage != crate::index::Stage::Normal {
return Err(RecursiveFailure::Unmerged);
}
if depth >= self.options.max_depth.min(64) {
return Err(RecursiveFailure::Limit("depth"));
}
if self.report.entries.len() >= self.options.max_submodules {
return Err(RecursiveFailure::Limit("count"));
}
let entry = self.report.entries.len();
self.report.entries.push(RecursiveEntry {
path: self.path.clone(),
initialized: false,
population: None,
});
let initialized = definition
.plan_init(parent.config(), url)
.map_err(prepare)?
.apply(&parent)
.map_err(prepare)?;
self.report.entries[entry].initialized = initialized;
parent = parent.reopen().map_err(prepare)?;
let plan = definition
.plan_update(parent.config(), link.id, Commands::Refuse)
.map_err(prepare)?;
let skip = matches!(plan.method(), UpdateMethod::BuiltIn(Update::None));
let report = plan
.populate(&parent, &self.options.update, self.callbacks)
.map_err(|error| RecursiveFailure::Populate(Box::new(error)))?;
self.report.entries[entry].population = Some(report);
if !skip {
let path = super::status::checkout_path(&parent, plan.path())
.map_err(prepare)?
.ok_or(RecursiveFailure::NoWorktree)?;
let child = Repository::open_with_config(path, parent.inherited_config())
.map_err(prepare)?;
let next = self.path.clone();
self.visit(&child, plan.url(), &next, depth + 1)?;
}
self.path = prefix.to_vec();
if !parent
.index_unchanged(Some(&index), limits)
.map_err(prepare)?
{
return Err(RecursiveFailure::Changed);
}
}
self.path = prefix.to_vec();
if !parent
.index_unchanged(Some(&index), limits)
.map_err(prepare)?
{
return Err(RecursiveFailure::Changed);
}
Ok(())
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn joined(prefix: &[u8], path: &[u8]) -> Vec<u8> {
if prefix.is_empty() {
return path.to_vec();
}
[prefix, b"/", path].concat()
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn prepare(error: impl Into<crate::Error>) -> RecursiveFailure {
RecursiveFailure::Prepare(Box::new(error.into()))
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn selected(
config: &crate::Config,
definition: &super::Definition,
) -> Result<bool, RecursiveFailure> {
if let Some(active) = config
.boolean("submodule", Some(definition.name()), "active")
.map_err(prepare)?
{
return Ok(active);
}
if config.values("submodule", None, "active").next().is_none() {
return Ok(true);
}
super::plan::active(
config,
definition.name(),
definition.path().ok_or(RecursiveFailure::Definition)?,
)
.map_err(prepare)
}