use crate::fleet::error::{Result, SuperGitError};
use crate::fleet::git::operations::GitOps;
use crate::fleet::git::tree::{SubmoduleNode, SubmoduleTree, MAX_SUBMODULE_DEPTH};
use crate::git_backend;
use crate::prelude::*;
#[derive(Debug, Clone)]
pub struct SubmoduleStatus {
pub name: String,
pub path: String,
pub current_branch: Option<String>,
pub is_clean: bool,
pub is_detached: bool,
pub ahead: usize,
pub behind: usize,
pub uncommitted_files: Vec<String>,
pub rendered_files: Vec<String>,
pub moved_pointers: Vec<String>,
pub exists: bool,
pub depth: usize,
}
#[derive(Debug, Default)]
pub struct InitOutcome {
pub initialized: Vec<String>,
pub failures: Vec<(String, SuperGitError)>,
}
pub fn split_rendered(
repo_root: &Path,
changed: Vec<String>,
rendered: &std::collections::HashSet<PathBuf>,
) -> (Vec<String>, Vec<String>) {
changed
.into_iter()
.partition(|path| !rendered.contains(&repo_root.join(path)))
}
pub struct SubmoduleOps;
impl SubmoduleOps {
pub fn get_status(
node: &SubmoduleNode,
rendered: &std::collections::HashSet<PathBuf>,
) -> Result<SubmoduleStatus> {
if !node.initialized {
return Ok(SubmoduleStatus {
name: node.display_path.clone(),
path: node.display_path.clone(),
current_branch: None,
is_clean: true,
is_detached: false,
ahead: 0,
behind: 0,
uncommitted_files: Vec::new(),
rendered_files: Vec::new(),
moved_pointers: Vec::new(),
exists: false,
depth: node.depth + 1,
});
}
let repo = GitOps::open_repo(&node.abs_path)?;
let is_detached = GitOps::is_detached_head(&repo)?;
let current_branch = if is_detached {
None
} else {
Some(GitOps::current_branch(&repo)?)
};
let (changed, moved_pointers) = GitOps::uncommitted_files_and_pointers(&repo)?;
let (uncommitted_files, rendered_files) =
split_rendered(&node.abs_path, changed, rendered);
let is_clean = uncommitted_files.is_empty();
let (ahead, behind) = if !is_detached {
GitOps::get_ahead_behind(&repo).unwrap_or((0, 0))
} else {
(0, 0)
};
Ok(SubmoduleStatus {
name: node.display_path.clone(),
path: node.display_path.clone(),
current_branch,
is_clean,
is_detached,
ahead,
behind,
uncommitted_files,
rendered_files,
moved_pointers,
exists: true,
depth: node.depth + 1,
})
}
pub fn init(node: &SubmoduleNode) -> Result<()> {
let parent = node.abs_path.parent().ok_or_else(|| {
SuperGitError::Other(format!(
"Submodule path '{}' does not have a parent directory",
node.display_path
))
})?;
std::fs::create_dir_all(parent).map_err(|err| {
SuperGitError::Other(format!(
"Failed to create parent directory '{}': {}",
parent.display(),
err
))
})?;
let super_repo = git_backend::open_repo(&node.parent_path)?;
let gitlink_commit = super_repo
.head_commit()
.map_err(|err| SuperGitError::Other(err.to_string()))?
.tree()
.map_err(|err| SuperGitError::Other(err.to_string()))?
.lookup_entry_by_path(&node.rel_path)
.map_err(|err| SuperGitError::Other(err.to_string()))?
.ok_or_else(|| {
SuperGitError::Other(format!(
"Submodule path '{}' is not present in the current tree",
node.display_path
))
})?;
if !gitlink_commit.mode().is_commit() {
return Err(SuperGitError::Other(format!(
"Path '{}' is not a gitlink entry in its superproject",
node.display_path
)));
}
let url = node.resolved_url()?;
let repo = git_backend::clone_repository_in(&url, &node.abs_path, &node.parent_path)?;
git_backend::checkout_commit(&repo, gitlink_commit.id().detach())?;
Ok(())
}
pub fn init_tree<P: AsRef<Path>>(
root: P,
recursive: bool,
mut on_init: impl FnMut(&SubmoduleNode),
) -> Result<InitOutcome> {
let root = root.as_ref();
let mut outcome = InitOutcome::default();
let mut attempted: std::collections::HashSet<String> = std::collections::HashSet::new();
for _ in 0..=MAX_SUBMODULE_DEPTH {
let tree = SubmoduleTree::discover_allow_empty(root)?;
let pending: Vec<SubmoduleNode> = tree
.top_down()
.iter()
.filter(|node| !node.initialized)
.filter(|node| recursive || node.depth == 0)
.filter(|node| !attempted.contains(&node.display_path))
.cloned()
.collect();
if pending.is_empty() {
return Ok(outcome);
}
for node in &pending {
attempted.insert(node.display_path.clone());
on_init(node);
match Self::init(node) {
Ok(()) => outcome.initialized.push(node.display_path.clone()),
Err(err) => outcome.failures.push((node.display_path.clone(), err)),
}
}
if !recursive {
return Ok(outcome);
}
}
Err(SuperGitError::Other(format!(
"Submodule initialization did not converge within {MAX_SUBMODULE_DEPTH} passes at \
'{}' — check .gitmodules for a cycle",
root.display()
)))
}
pub fn checkout(node: &SubmoduleNode, branch: &str, create: bool) -> Result<()> {
let repo = Self::open(node)?;
if create {
git_backend::create_branch(&repo, branch, None)?;
} else {
git_backend::checkout_branch(&repo, branch)?;
}
Ok(())
}
pub fn pull(
node: &SubmoduleNode,
options: git_backend::PullOptions,
) -> Result<git_backend::PullOutcome> {
let repo = Self::open(node)?;
git_backend::pull(&repo, options).map_err(|err| {
SuperGitError::Other(format!(
"Failed to pull in '{}': {}",
node.display_path, err
))
})
}
pub fn push(node: &SubmoduleNode, force_with_lease: bool) -> Result<()> {
let repo = Self::open(node)?;
git_backend::push_current_branch(&repo, Some("origin"), true, force_with_lease)?;
Ok(())
}
pub fn commit(node: &SubmoduleNode, message: &str) -> Result<()> {
let repo = Self::open(node)?;
git_backend::commit_all(&repo, message)?;
Ok(())
}
pub fn create_branch(
node: &SubmoduleNode,
branch: &str,
from_branch: Option<&str>,
) -> Result<()> {
let repo = Self::open(node)?;
git_backend::create_branch(&repo, branch, from_branch)?;
Ok(())
}
pub fn delete_branch(node: &SubmoduleNode, branch: &str, force: bool) -> Result<()> {
let repo = Self::open(node)?;
git_backend::delete_local_branch(&repo, branch, force)?;
Ok(())
}
pub fn delete_remote_branch(node: &SubmoduleNode, branch: &str) -> Result<()> {
let repo = Self::open(node)?;
git_backend::delete_remote_branch(&repo, Some("origin"), branch)?;
Ok(())
}
pub fn stage(node: &SubmoduleNode) -> Result<()> {
let repo = Self::open(node)?;
git_backend::stage_all(&repo)?;
Ok(())
}
pub fn merge_ff(node: &SubmoduleNode, branch: &str) -> Result<()> {
let repo = Self::open(node)?;
git_backend::merge_ff(&repo, branch)?;
Ok(())
}
pub fn fetch(node: &SubmoduleNode, all_remotes: bool) -> Result<()> {
let repo = Self::open(node)?;
if all_remotes {
git_backend::fetch_all(&repo)?;
} else {
git_backend::fetch(&repo, Some("origin"))?;
}
Ok(())
}
fn open(node: &SubmoduleNode) -> Result<gix::Repository> {
if !node.initialized {
return Err(SuperGitError::SubmoduleNotFound(node.display_path.clone()));
}
git_backend::open_repo(&node.abs_path).map_err(|err| {
SuperGitError::Other(format!("Failed to open '{}': {}", node.display_path, err))
})
}
}