use std::path::PathBuf;
use super::{UpdateOptions, UpdatePlan, UpdateReport};
use crate::{Repository, workflow};
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct PopulateReport {
pub cloned: Option<Box<workflow::CloneOutcome>>,
pub fetched: Option<workflow::FetchOutcome>,
pub worktree_configured: bool,
pub gitfile_written: bool,
pub update: Option<UpdateReport>,
}
#[derive(Debug, thiserror::Error)]
#[error("submodule population failed: {source}")]
pub struct PopulateError {
pub report: Box<PopulateReport>,
#[source]
pub source: PopulateFailure,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PopulateFailure {
#[error(transparent)]
Update(#[from] super::UpdateError),
#[error("submodule destination is occupied or unsafe: {}", .0.display())]
Occupied(PathBuf),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Prepare(Box<crate::Error>),
#[error("shallow submodule cloning is not supported")]
Shallow,
#[error("submodule URL is not UTF-8")]
UrlEncoding,
#[error(transparent)]
Clone(Box<workflow::CloneError>),
#[error(transparent)]
Fetch(Box<workflow::TransferError>),
#[error("recorded submodule commit {0} was not fetched from advertised heads or tags")]
MissingCommit(crate::ObjectId),
}
impl UpdatePlan {
pub fn populate(
&self,
parent: &Repository,
options: &UpdateOptions,
callbacks: &mut dyn workflow::TransferCallbacks,
) -> Result<PopulateReport, PopulateError> {
let mut report = PopulateReport::default();
if matches!(
self.method(),
super::UpdateMethod::BuiltIn(super::Update::None)
) {
report.update = Some(UpdateReport::default());
return Ok(report);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
let result = self.populate_inner(parent, options, callbacks, &mut report);
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
let result: Result<(), PopulateFailure> = {
let _ = (parent, options, callbacks);
Err(super::UpdateError::UnsupportedPlatform.into())
};
match result {
Ok(()) => Ok(report),
Err(source) => Err(PopulateError {
report: Box::new(report),
source,
}),
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn populate_inner(
&self,
parent: &Repository,
options: &UpdateOptions,
callbacks: &mut dyn workflow::TransferCallbacks,
report: &mut PopulateReport,
) -> Result<(), PopulateFailure> {
use std::io::Write;
use std::os::unix::ffi::OsStrExt;
use std::sync::atomic::AtomicBool;
use super::{Update, UpdateMethod};
match self.method() {
UpdateMethod::BuiltIn(Update::None) => {
report.update = Some(UpdateReport::default());
return Ok(());
}
UpdateMethod::BuiltIn(Update::Checkout) => {}
_ => return Err(super::UpdateError::UnsupportedMethod.into()),
}
let fresh = parent.reopen().map_err(prepare)?;
if !super::execute::same_layout(parent, &fresh)
|| self.expected != super::plan::update_values(fresh.config(), self.name())
{
return Err(super::UpdateError::Changed.into());
}
let parent = &fresh;
let index = parent.read_index(options.limits.index).map_err(prepare)?;
let mut entries = index
.as_ref()
.into_iter()
.flat_map(|i| i.entries())
.filter(|e| e.path == self.path());
let entry = entries.next().ok_or(super::UpdateError::Changed)?;
if entry.id != self.recorded()
|| entry.mode != crate::index::Mode::Gitlink
|| entry.stage != crate::index::Stage::Normal
|| entries.next().is_some()
{
return Err(super::UpdateError::Changed.into());
}
let worktree = parent
.worktree()
.ok_or(super::UpdateError::MissingCheckout)?;
let child_path = worktree.join(std::ffi::OsStr::from_bytes(self.path()));
if child_path
.as_os_str()
.as_bytes()
.iter()
.any(u8::is_ascii_control)
|| parent
.common_dir()
.as_os_str()
.as_bytes()
.iter()
.any(u8::is_ascii_control)
|| (cfg!(target_os = "macos") && (!self.path().is_ascii() || !self.name().is_ascii()))
{
return Err(PopulateFailure::Occupied(child_path));
}
let populated = super::status::checkout_path(parent, self.path())
.map_err(prepare)?
.is_some();
let environment = options
.environment
.clone()
.with_current_dir(worktree)
.with("GIT_PROTOCOL_FROM_USER", "0");
if populated {
let child = Repository::open_with_config(&child_path, parent.inherited_config())
.map_err(prepare)?;
if child.is_bare() || child.worktree() != Some(child_path.as_path()) {
return Err(PopulateFailure::Occupied(child_path));
}
self.recorded()
.require_format(child.object_format())
.map_err(prepare)?;
match child
.objects_with(options.limits.packs)
.map_err(prepare)?
.read_commit_with(self.recorded(), options.limits.read)
{
Ok(_) => {}
Err(crate::ReadCommitError::Missing(_)) => {
let environment = super::child_environment(&child, &environment)
.with_current_dir(&child_path);
let fetch = workflow::FetchOptions::new([
"+refs/heads/*:refs/remotes/origin/*",
"refs/tags/*:refs/tags/*",
]);
report.fetched = Some(
workflow::fetch(&child, "origin", &fetch, &environment, callbacks)
.map_err(|e| PopulateFailure::Fetch(Box::new(e)))?,
);
let child = child.reopen().map_err(prepare)?;
match child
.objects_with(options.limits.packs)
.map_err(prepare)?
.read_commit_with(self.recorded(), options.limits.read)
{
Ok(_) => {}
Err(crate::ReadCommitError::Missing(_)) => {
return Err(PopulateFailure::MissingCommit(self.recorded()));
}
Err(e) => return Err(prepare(e)),
}
}
Err(e) => return Err(prepare(e)),
}
report.update = Some(self.apply(parent, options)?);
return Ok(());
}
if self.shallow() {
return Err(PopulateFailure::Shallow);
}
check_empty_path(worktree, self.path())?;
let modules = parent.common_dir().join("modules");
check_storage(&modules, self.name())?;
create_directories(worktree, self.path())?;
let storage_parent = self.name().rsplitn(2, |b| *b == b'/').nth(1);
ensure_directory(&modules)?;
if let Some(prefix) = storage_parent {
create_directories(&modules, prefix)?;
}
let destination = modules.join(std::ffi::OsStr::from_bytes(self.name()));
let url = std::str::from_utf8(self.url()).map_err(|_| PopulateFailure::UrlEncoding)?;
let cloned = workflow::clone_with(
url,
&destination,
&workflow::CloneOptions::default()
.with_kind(crate::InitKind::Bare)
.with_checkout(false),
parent.inherited_config(),
&environment,
callbacks,
)
.map_err(|e| PopulateFailure::Clone(Box::new(e)))?;
report.cloned = Some(Box::new(cloned));
let child = Repository::open_with_config(&destination, parent.inherited_config())
.map_err(prepare)?;
self.recorded()
.require_format(child.object_format())
.map_err(prepare)?;
let mut edit = child.edit_config(1024 * 1024).map_err(prepare)?;
edit.document_mut()
.append("core", None, "bare", b"false")
.map_err(prepare)?;
edit.document_mut()
.append("core", None, "worktree", child_path.as_os_str().as_bytes())
.map_err(prepare)?;
edit.commit().map_err(prepare)?;
report.worktree_configured = true;
let mut gitfile = std::fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(child_path.join(".git"))?;
report.gitfile_written = true;
gitfile.write_all(b"gitdir: ")?;
gitfile.write_all(destination.as_os_str().as_bytes())?;
gitfile.write_all(b"\n")?;
report.update =
Some(self.checkout_initial(parent, options, &AtomicBool::new(false), true, || {})?);
Ok(())
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn prepare(error: impl Into<crate::Error>) -> PopulateFailure {
PopulateFailure::Prepare(Box::new(error.into()))
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn ensure_directory(path: &std::path::Path) -> Result<(), PopulateFailure> {
match std::fs::symlink_metadata(path) {
Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => Ok(()),
Ok(_) => Err(PopulateFailure::Occupied(path.to_owned())),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
Ok(std::fs::create_dir(path)?)
}
Err(error) => Err(error.into()),
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn check_empty_path(root: &std::path::Path, path: &[u8]) -> Result<(), PopulateFailure> {
use std::os::unix::ffi::OsStrExt;
let mut current = root.to_owned();
for component in path.split(|b| *b == b'/') {
let native = std::ffi::OsStr::from_bytes(component);
if current.exists() {
for entry in std::fs::read_dir(¤t)? {
let name = entry?.file_name();
if cfg!(target_os = "macos")
&& name != native
&& name.as_bytes().eq_ignore_ascii_case(component)
{
return Err(PopulateFailure::Occupied(current.join(native)));
}
}
}
current.push(native);
match std::fs::symlink_metadata(¤t) {
Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => {}
Ok(_) => return Err(PopulateFailure::Occupied(current)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(error.into()),
}
}
if std::fs::read_dir(¤t)?.next().is_some() {
return Err(PopulateFailure::Occupied(current));
}
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn check_storage(root: &std::path::Path, name: &[u8]) -> Result<(), PopulateFailure> {
use std::os::unix::ffi::OsStrExt;
if cfg!(target_os = "macos") {
let parent = root
.parent()
.ok_or_else(|| PopulateFailure::Occupied(root.to_owned()))?;
for entry in std::fs::read_dir(parent)? {
let name = entry?.file_name();
if name.as_bytes() != b"modules" && name.as_bytes().eq_ignore_ascii_case(b"modules") {
return Err(PopulateFailure::Occupied(root.to_owned()));
}
}
}
match std::fs::symlink_metadata(root) {
Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => {}
Ok(_) => return Err(PopulateFailure::Occupied(root.to_owned())),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(error.into()),
}
let mut current = root.to_owned();
for component in name.split(|b| *b == b'/') {
if current.join("HEAD").exists() || current.join("config").exists() {
return Err(PopulateFailure::Occupied(current));
}
let native = std::ffi::OsStr::from_bytes(component);
for entry in std::fs::read_dir(¤t)? {
let name = entry?.file_name();
if cfg!(target_os = "macos")
&& name != native
&& name.as_bytes().eq_ignore_ascii_case(component)
{
return Err(PopulateFailure::Occupied(current.join(native)));
}
}
current.push(native);
match std::fs::symlink_metadata(¤t) {
Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => {}
Ok(_) => return Err(PopulateFailure::Occupied(current)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(error.into()),
}
}
Err(PopulateFailure::Occupied(current))
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn create_directories(root: &std::path::Path, path: &[u8]) -> Result<(), PopulateFailure> {
use std::os::unix::ffi::OsStrExt;
let mut current = root.to_owned();
for component in path.split(|b| *b == b'/') {
current.push(std::ffi::OsStr::from_bytes(component));
ensure_directory(¤t)?;
}
Ok(())
}