use color_eyre::{
eyre::{bail, Context},
Result,
};
use git2::{Cred, ErrorClass, ErrorCode, FetchOptions, RemoteCallbacks, Repository};
use std::{
fs,
path::{Path, PathBuf},
};
pub fn fetch(repo: &Repository, remote: &str, version: &str) -> Result<(), git2::Error> {
let cb = {
let mut cb = RemoteCallbacks::new();
cb.credentials(|_url, username, _allowed_types| {
Cred::ssh_key_from_agent(username.unwrap())
});
cb
};
let mut fetch_opts = FetchOptions::new();
fetch_opts.remote_callbacks(cb);
repo.find_remote(remote)?
.fetch(&[version], Some(&mut fetch_opts), None)?;
Ok(())
}
pub fn checkout_to_spec(repo: &Repository, spec: &str, checkout: bool) -> Result<(), git2::Error> {
let (obj, ref_) = repo.revparse_ext(spec)?;
if checkout {
repo.checkout_tree(&obj, None)?;
}
match ref_ {
Some(ref_) => repo.set_head(ref_.name().unwrap())?,
None => repo.set_head_detached(obj.id())?,
}
Ok(())
}
pub fn checkout_to_version(
repo: &Repository,
version: &str,
checkout: bool,
) -> Result<(), git2::Error> {
let result = checkout_to_spec(repo, version, checkout);
match result {
Ok(()) => {}
Err(err) if err.class() == ErrorClass::Reference && err.code() == ErrorCode::NotFound => {
let spec = format!("origin/{version}");
checkout_to_spec(repo, &spec, checkout)?;
}
Err(err) => return Err(err),
}
Ok(())
}
pub fn remove_submodule(repo: &Repository, path: &Path) -> Result<()> {
let path_str = path.to_string_lossy();
let submodule = repo
.find_submodule(&path_str)
.with_context(|| format!("Failed to find submodule {path_str}"))?;
let name = submodule
.name()
.ok_or_else(|| color_eyre::eyre::eyre!("Submodule has no name"))?;
{
let mut config = repo.config()?;
let section = format!("submodule.{name}");
let _ = config.remove(&format!("{section}.url"));
let _ = config.remove(&format!("{section}.update"));
let _ = config.remove(&format!("{section}.branch"));
let _ = config.remove(&format!("{section}.fetchRecurseSubmodules"));
let _ = config.remove(&format!("{section}.ignore"));
}
{
let mut index = repo.index()?;
index.remove_path(path)?;
index.write()?;
}
update_gitmodules_file(repo, path, name, true)?;
let modules_path = repo.path().join("modules").join(name);
if modules_path.exists() {
fs::remove_dir_all(&modules_path)
.with_context(|| format!("Failed to remove modules directory for {name}"))?;
}
if path.exists() {
fs::remove_dir_all(path)
.with_context(|| format!("Failed to remove working directory {path_str}"))?;
}
Ok(())
}
pub fn remove_submodule_rollback(repo: &Repository, path: &Path) -> Result<()> {
let path_str = path.to_string_lossy();
let submodule_name = if let Ok(submodule) = repo.find_submodule(&path_str) {
submodule.name().map(|s| s.to_string())
} else {
None
};
if let Some(ref name) = submodule_name {
if let Ok(mut config) = repo.config() {
let section = format!("submodule.{name}");
let _ = config.remove(&format!("{section}.url"));
let _ = config.remove(&format!("{section}.update"));
let _ = config.remove(&format!("{section}.branch"));
let _ = config.remove(&format!("{section}.fetchRecurseSubmodules"));
let _ = config.remove(&format!("{section}.ignore"));
}
}
if let Ok(mut index) = repo.index() {
let _ = index.remove_path(path);
let _ = index.write();
}
if update_gitmodules_file(
repo,
path,
submodule_name.as_deref().unwrap_or(&path_str),
false,
)
.is_err()
{
manually_clean_gitmodules(repo, path)?;
}
if let Some(ref name) = submodule_name {
let modules_path = repo.path().join("modules").join(name);
if modules_path.exists() {
let _ = fs::remove_dir_all(&modules_path);
}
}
let modules_path = repo.path().join("modules").join(path);
if modules_path.exists() {
let _ = fs::remove_dir_all(&modules_path);
}
if path.exists() {
let _ = fs::remove_dir_all(path);
}
Ok(())
}
fn update_gitmodules_file(
repo: &Repository,
path: &Path,
name: &str,
must_exist: bool,
) -> Result<()> {
let gitmodules_path = PathBuf::from(".gitmodules");
if !gitmodules_path.exists() {
if must_exist {
bail!(".gitmodules file not found");
}
return Ok(());
}
let content = fs::read_to_string(&gitmodules_path)?;
let mut new_lines = Vec::new();
let mut in_target_section = false;
let mut found_section = false;
let section_header = format!("[submodule \"{name}\"]");
let alt_section_header = format!("[submodule \"{}\"]", path.to_string_lossy());
for line in content.lines() {
let trimmed = line.trim();
if trimmed == section_header || trimmed == alt_section_header {
in_target_section = true;
found_section = true;
continue;
}
if in_target_section && trimmed.starts_with('[') {
in_target_section = false;
}
if !in_target_section {
new_lines.push(line);
}
}
if must_exist && !found_section {
bail!("Submodule section not found in .gitmodules");
}
let new_content = new_lines.join("\n");
let trimmed_content = new_content.trim();
if trimmed_content.is_empty() || trimmed_content.is_empty() {
fs::remove_file(&gitmodules_path)?;
let mut index = repo.index()?;
index.remove_path(Path::new(".gitmodules"))?;
index.write()?;
} else {
fs::write(&gitmodules_path, trimmed_content)?;
let mut index = repo.index()?;
index.add_path(Path::new(".gitmodules"))?;
index.write()?;
}
Ok(())
}
fn manually_clean_gitmodules(repo: &Repository, path: &Path) -> Result<()> {
let gitmodules_path = PathBuf::from(".gitmodules");
if !gitmodules_path.exists() {
return Ok(());
}
let content = fs::read_to_string(&gitmodules_path)?;
let mut new_content = String::new();
let mut in_submodule_section = false;
let path_str = path.to_string_lossy();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("[submodule ") && trimmed.ends_with("]") {
if trimmed.contains(&*path_str) {
in_submodule_section = true;
continue;
}
}
if in_submodule_section && trimmed.starts_with('[') {
in_submodule_section = false;
}
if !in_submodule_section {
new_content.push_str(line);
new_content.push('\n');
}
}
let new_content = new_content.trim();
if new_content.is_empty() {
fs::remove_file(&gitmodules_path)?;
if let Ok(mut index) = repo.index() {
let _ = index.remove_path(Path::new(".gitmodules"));
let _ = index.write();
}
} else {
fs::write(&gitmodules_path, new_content)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_repo() -> Result<(TempDir, Repository)> {
let dir = TempDir::new()?;
let repo = Repository::init(dir.path())?;
let sig = git2::Signature::now("Test User", "test@example.com")?;
let tree_id = {
let mut index = repo.index()?;
index.write_tree()?
};
let tree = repo.find_tree(tree_id)?;
repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])?;
drop(tree); Ok((dir, repo))
}
#[test]
fn test_checkout_to_spec_basic() {
let (_dir, repo) = create_test_repo().unwrap();
assert!(checkout_to_spec(&repo, "HEAD", false).is_ok());
}
}