use git2::{Repository, SubmoduleIgnore};
use log::{debug, error, warn};
use std::path::Path;
#[derive(Debug)]
pub struct SubmodulesDiagnostic {
pub modified_not_staged_submodules: Vec<String>,
pub modified_staged_submodules: Vec<String>,
}
pub fn check_submodules(strict: bool, path: &Path) -> anyhow::Result<Option<SubmodulesDiagnostic>> {
if let Ok(repo) = Repository::open(path) {
if let Ok(submodules) = repo.submodules() {
let mut modified_not_staged_submodules: Vec<String> = vec![];
let mut modified_staged_submodules: Vec<String> = vec![];
for submodule in submodules {
if let Some(name) = submodule.name() {
debug!("checking submodule: {name}");
let status = repo.submodule_status(name, SubmoduleIgnore::None)?;
if status.is_wd_modified() {
debug!("{name} is modified but not staged");
modified_not_staged_submodules.push(String::from(name));
}
if status.is_index_modified() {
debug!("{name} is modified and staged");
modified_staged_submodules.push(String::from(name));
}
} else {
warn!("submodule does not have a name");
}
}
return Ok(Some(SubmodulesDiagnostic {
modified_not_staged_submodules,
modified_staged_submodules,
}));
} else {
error!("failed to list submodules");
if strict {
anyhow::bail!("Failed to list submodules.");
}
}
} else {
error!("failed to open git repository");
if strict {
anyhow::bail!("Unable to open repository");
}
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
use git2::{IndexAddOption, Oid, Signature};
use std::fs;
use tempfile::TempDir;
fn create_temp_repo() -> anyhow::Result<(TempDir, Repository)> {
let temp_dir = TempDir::new()?;
let temp_path = temp_dir.path().to_path_buf();
let repo = Repository::init(&temp_path)?;
let sig = Signature::now("Test User", "test@example.com")?;
let tree_id = {
let mut index = repo.index()?;
let readme_path = temp_path.join("README.md");
fs::write(&readme_path, "# Test Repository\n")?;
index.add_path(std::path::Path::new("README.md"))?;
index.write()?;
index.write_tree()?
};
let tree = repo.find_tree(tree_id)?;
repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])?;
drop(tree);
Ok((temp_dir, repo))
}
fn create_commit(repo: &Repository, message: &str) -> anyhow::Result<Oid> {
let sig = Signature::now("Test User", "test@example.com")?;
let file_name = format!("file_{}.txt", message.replace(' ', "_"));
let file_path = repo.workdir().unwrap().join(&file_name);
fs::write(&file_path, format!("Content for {}", message))?;
let mut index = repo.index()?;
index.add_path(std::path::Path::new(&file_name))?;
index.write()?;
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;
let parent_commit = repo.head()?.peel_to_commit()?;
let oid = repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&parent_commit])?;
Ok(oid)
}
fn add_submodule(
parent_repo: &Repository,
submodule_name: &str,
) -> anyhow::Result<(TempDir, String)> {
let (submodule_temp_dir, submodule_repo) = create_temp_repo()?;
let submodule_path = submodule_temp_dir.path().to_path_buf();
let submodule_url = format!("file://{}", submodule_path.display());
let mut submodule =
parent_repo.submodule(&submodule_url, std::path::Path::new(submodule_name), false)?;
let _cloned_repo = submodule.clone(None)?;
submodule.add_finalize()?;
let sig = Signature::now("Test User", "test@example.com")?;
let mut index = parent_repo.index()?;
index.add_all(
[".gitmodules", submodule_name].iter(),
IndexAddOption::DEFAULT,
None,
)?;
index.write()?;
let tree_id = index.write_tree()?;
let tree = parent_repo.find_tree(tree_id)?;
let parent_commit = parent_repo.head()?.peel_to_commit()?;
parent_repo.commit(
Some("HEAD"),
&sig,
&sig,
&format!("Add submodule {}", submodule_name),
&tree,
&[&parent_commit],
)?;
drop(tree);
drop(submodule_repo);
Ok((submodule_temp_dir, submodule_name.to_string()))
}
fn modify_submodule_wd(parent_repo: &Repository, submodule_name: &str) -> anyhow::Result<()> {
let submodule_path = parent_repo.workdir().unwrap().join(submodule_name);
let submodule_repo = Repository::open(&submodule_path)?;
create_commit(&submodule_repo, "Submodule modification")?;
Ok(())
}
fn stage_submodule(parent_repo: &Repository, submodule_name: &str) -> anyhow::Result<()> {
let mut index = parent_repo.index()?;
index.add_path(std::path::Path::new(submodule_name))?;
index.write()?;
Ok(())
}
#[test]
fn test_create_temp_repo() {
let result = create_temp_repo();
assert!(result.is_ok());
let (_temp_dir, repo) = result.unwrap();
assert!(!repo.is_bare());
assert!(repo.head().is_ok());
let head = repo.head().unwrap();
let commit = head.peel_to_commit().unwrap();
assert_eq!(commit.message().unwrap(), "Initial commit");
}
#[test]
fn test_create_commit() {
let (_temp_dir, repo) = create_temp_repo().unwrap();
let result = create_commit(&repo, "Test commit");
assert!(result.is_ok());
let commit = repo.head().unwrap().peel_to_commit().unwrap();
assert_eq!(commit.message().unwrap(), "Test commit");
}
#[test]
fn test_add_submodule() {
let (_parent_temp_dir, parent_repo) = create_temp_repo().unwrap();
let result = add_submodule(&parent_repo, "test-submodule");
assert!(result.is_ok());
let (_submodule_temp_dir, submodule_name) = result.unwrap();
let submodules = parent_repo.submodules().unwrap();
assert_eq!(submodules.len(), 1);
assert_eq!(submodules[0].name().unwrap(), submodule_name);
let submodule_path = parent_repo.workdir().unwrap().join(&submodule_name);
assert!(submodule_path.exists());
}
#[test]
fn test_modify_submodule_wd() {
let (_parent_temp_dir, parent_repo) = create_temp_repo().unwrap();
let (_submodule_temp_dir, submodule_name) =
add_submodule(&parent_repo, "test-submodule").unwrap();
let result = modify_submodule_wd(&parent_repo, &submodule_name);
assert!(result.is_ok());
let status = parent_repo
.submodule_status(&submodule_name, SubmoduleIgnore::None)
.unwrap();
assert!(status.is_wd_modified());
}
#[test]
fn test_stage_submodule() {
let (_parent_temp_dir, parent_repo) = create_temp_repo().unwrap();
let (_submodule_temp_dir, submodule_name) =
add_submodule(&parent_repo, "test-submodule").unwrap();
modify_submodule_wd(&parent_repo, &submodule_name).unwrap();
let result = stage_submodule(&parent_repo, &submodule_name);
assert!(result.is_ok());
let status = parent_repo
.submodule_status(&submodule_name, SubmoduleIgnore::None)
.unwrap();
assert!(status.is_index_modified());
}
#[test]
fn test_no_submodules() {
let (_temp_dir, repo) = create_temp_repo().unwrap();
let repo_path = repo.workdir().unwrap();
let result = check_submodules(false, repo_path);
assert!(result.is_ok());
let diagnostic = result.unwrap();
assert!(diagnostic.is_some());
let diagnostic = diagnostic.unwrap();
assert!(diagnostic.modified_not_staged_submodules.is_empty());
assert!(diagnostic.modified_staged_submodules.is_empty());
}
#[test]
fn test_unmodified_submodules() {
let (_parent_temp_dir, parent_repo) = create_temp_repo().unwrap();
let (_submodule_temp_dir, _submodule_name) =
add_submodule(&parent_repo, "clean-submodule").unwrap();
let repo_path = parent_repo.workdir().unwrap();
let result = check_submodules(false, repo_path);
assert!(result.is_ok());
let diagnostic = result.unwrap().unwrap();
assert!(diagnostic.modified_not_staged_submodules.is_empty());
assert!(diagnostic.modified_staged_submodules.is_empty());
}
#[test]
fn test_modified_not_staged_submodule() {
let (_parent_temp_dir, parent_repo) = create_temp_repo().unwrap();
let (_submodule_temp_dir, submodule_name) =
add_submodule(&parent_repo, "modified-submodule").unwrap();
modify_submodule_wd(&parent_repo, &submodule_name).unwrap();
let repo_path = parent_repo.workdir().unwrap();
let result = check_submodules(false, repo_path);
assert!(result.is_ok());
let diagnostic = result.unwrap().unwrap();
assert_eq!(diagnostic.modified_not_staged_submodules.len(), 1);
assert_eq!(diagnostic.modified_not_staged_submodules[0], submodule_name);
assert!(diagnostic.modified_staged_submodules.is_empty());
}
#[test]
fn test_modified_staged_submodule() {
let (_parent_temp_dir, parent_repo) = create_temp_repo().unwrap();
let (_submodule_temp_dir, submodule_name) =
add_submodule(&parent_repo, "staged-submodule").unwrap();
modify_submodule_wd(&parent_repo, &submodule_name).unwrap();
stage_submodule(&parent_repo, &submodule_name).unwrap();
let repo_path = parent_repo.workdir().unwrap();
let result = check_submodules(false, repo_path);
assert!(result.is_ok());
let diagnostic = result.unwrap().unwrap();
assert!(diagnostic.modified_not_staged_submodules.is_empty());
assert_eq!(diagnostic.modified_staged_submodules.len(), 1);
assert_eq!(diagnostic.modified_staged_submodules[0], submodule_name);
}
#[test]
fn test_both_modified_submodules() {
let (_parent_temp_dir, parent_repo) = create_temp_repo().unwrap();
let (_submodule1_temp_dir, submodule1_name) =
add_submodule(&parent_repo, "submodule1").unwrap();
modify_submodule_wd(&parent_repo, &submodule1_name).unwrap();
let (_submodule2_temp_dir, submodule2_name) =
add_submodule(&parent_repo, "submodule2").unwrap();
modify_submodule_wd(&parent_repo, &submodule2_name).unwrap();
stage_submodule(&parent_repo, &submodule2_name).unwrap();
let repo_path = parent_repo.workdir().unwrap();
let result = check_submodules(false, repo_path);
assert!(result.is_ok());
let diagnostic = result.unwrap().unwrap();
assert_eq!(diagnostic.modified_not_staged_submodules.len(), 1);
assert_eq!(
diagnostic.modified_not_staged_submodules[0],
submodule1_name
);
assert_eq!(diagnostic.modified_staged_submodules.len(), 1);
assert_eq!(diagnostic.modified_staged_submodules[0], submodule2_name);
}
#[test]
fn test_strict_mode_invalid_repo() {
let temp_dir = TempDir::new().unwrap();
let invalid_path = temp_dir.path();
let result = check_submodules(true, invalid_path);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Unable to open repository")
);
}
#[test]
fn test_non_strict_mode_invalid_repo() {
let temp_dir = TempDir::new().unwrap();
let invalid_path = temp_dir.path();
let result = check_submodules(false, invalid_path);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
}