use anyhow::{Context, Result};
use gix::bstr::ByteSlice;
use std::collections::HashMap;
use std::path::Path;
use crate::git_ops::simple_gix::fetch_repo;
fn gix_file_from_bytes(bytes: Vec<u8>) -> Result<gix::config::File<'static>> {
let mut owned_bytes: Vec<u8> = bytes;
gix::config::File::from_bytes_owned(
&mut owned_bytes,
gix::config::file::Metadata::from(gix::config::Source::Local),
Default::default(),
)
.map_err(|e| anyhow::anyhow!("Failed to parse gix config file: {e}"))
}
use super::{DetailedSubmoduleStatus, GitConfig, GitOperations, SubmoduleStatusFlags};
use crate::config::{SubmoduleAddOptions, SubmoduleEntries, SubmoduleUpdateOptions};
use crate::options::{ConfigLevel, GitmodulesConvert};
use crate::utilities;
#[derive(Debug, Clone, PartialEq)]
pub struct GixOperations {
repo: gix::Repository,
}
impl GixOperations {
pub fn new(repo_path: Option<&Path>) -> Result<Self> {
let repo = match repo_path {
Some(path) => gix::open(path)
.with_context(|| format!("Failed to open repository at {}", path.display()))?,
None => gix::discover(".")
.with_context(|| "Failed to discover repository in current directory")?,
};
Ok(Self { repo })
}
fn try_gix_operation<T, F>(&self, operation: F) -> Result<T>
where
F: FnOnce(&gix::Repository) -> Result<T>,
{
operation(&self.repo)
}
fn try_gix_operation_mut<T, F>(&mut self, operation: F) -> Result<T>
where
F: FnOnce(&mut gix::Repository) -> Result<T>,
{
operation(&mut self.repo)
}
fn convert_gitmodules_to_entries(
&self,
gitmodules: gix_submodule::File,
) -> Result<SubmoduleEntries> {
let as_config_file = gitmodules.into_config();
let mut sections_map = std::collections::HashMap::new();
for section in as_config_file.sections() {
let mut section_entries = std::collections::HashMap::new();
let name = if let Some(subsection) = section.header().subsection_name() {
subsection.to_string()
} else {
section.header().name().to_string()
};
let body_entries = section
.body()
.clone()
.into_iter()
.collect::<HashMap<_, _>>();
for (key, value) in body_entries {
section_entries.insert(key.to_string().clone(), value.to_string().clone());
}
sections_map.insert(name, section_entries);
}
let submodule_entries = crate::config::SubmoduleEntries::from_gitmodules(sections_map);
Ok(submodule_entries)
}
fn get_superproject_branch(&self) -> Result<String> {
self.repo
.head_ref()
.map_err(|e| anyhow::anyhow!("Failed to get HEAD reference: {e}"))?
.map(|r| r.name().shorten().to_string())
.ok_or_else(|| anyhow::anyhow!("HEAD is detached, not on a branch"))
}
#[allow(dead_code)]
fn convert_gix_status_to_flags(&self, status: &gix::submodule::Status) -> SubmoduleStatusFlags {
let mut flags = SubmoduleStatusFlags::empty();
if status.is_dirty() == Some(true) {
flags |= SubmoduleStatusFlags::WD_WD_MODIFIED;
}
flags
}
}
impl GitOperations for GixOperations {
fn read_gitmodules(&self) -> Result<SubmoduleEntries> {
let mutable_self = self.clone();
mutable_self.try_gix_operation(|repo| {
let gitmodules_path = repo
.workdir()
.ok_or_else(|| anyhow::anyhow!("Repository has no working directory"))?
.join(".gitmodules");
if !gitmodules_path.exists() {
return Ok(SubmoduleEntries::default());
}
let content = std::fs::read(&gitmodules_path)?;
let config = repo.config_snapshot();
let submodule_file =
gix_submodule::File::from_bytes(&content, Some(gitmodules_path), &config)?;
mutable_self.convert_gitmodules_to_entries(submodule_file)
})
}
fn write_gitmodules(&mut self, config: &SubmoduleEntries) -> Result<()> {
self.try_gix_operation(|repo| {
let mut git_config = gix::config::File::new(gix::config::file::Metadata::api());
for (name, entry) in config.submodule_iter() {
let subsection_name = name.as_bytes().as_bstr();
if let Some(path) = &entry.path {
git_config.set_raw_value_by(
"submodule",
Some(subsection_name),
"path",
path.as_bytes().as_bstr(),
)?;
}
if let Some(url) = &entry.url {
git_config.set_raw_value_by(
"submodule",
Some(subsection_name),
"url",
url.as_bytes().as_bstr(),
)?;
}
if let Some(branch) = &entry.branch {
let value = branch.to_string();
git_config.set_raw_value_by(
"submodule",
Some(subsection_name),
"branch",
value.as_bytes().as_bstr(),
)?;
}
if let Some(update) = &entry.update {
let value = update.to_gitmodules();
git_config.set_raw_value_by(
"submodule",
Some(subsection_name),
"update",
value.as_bytes().as_bstr(),
)?;
}
if let Some(ignore) = &entry.ignore {
let value = ignore.to_gitmodules();
git_config.set_raw_value_by(
"submodule",
Some(subsection_name),
"ignore",
value.as_bytes().as_bstr(),
)?;
}
if let Some(fetch_recurse) = &entry.fetch_recurse {
let value = fetch_recurse.to_gitmodules();
git_config.set_raw_value_by(
"submodule",
Some(subsection_name),
"fetchRecurseSubmodules",
value.as_bytes().as_bstr(),
)?;
}
}
let gitmodules_path = repo
.workdir()
.ok_or_else(|| anyhow::anyhow!("Repository has no working directory"))?
.join(".gitmodules");
let mut file = std::fs::File::create(&gitmodules_path)?;
git_config.write_to(&mut file)?;
Ok(())
})
}
fn read_git_config(&self, level: ConfigLevel) -> Result<GitConfig> {
self.clone().try_gix_operation_mut(|repo| {
let config_snapshot = repo.config_snapshot();
let mut entries = HashMap::new();
let source_filter = match level {
ConfigLevel::System => gix::config::Source::System,
ConfigLevel::Global => gix::config::Source::User,
ConfigLevel::Local => gix::config::Source::Local,
ConfigLevel::Worktree => gix::config::Source::Worktree,
};
for section in config_snapshot.sections() {
if section.meta().source == source_filter {
let section_name = section.header().name();
let body_iter = section.body().clone().into_iter();
for (key, value) in body_iter {
entries.insert(format!("{section_name}.{key}"), value.to_string());
}
}
}
Ok(GitConfig { entries })
})
}
fn write_git_config(&self, config: &GitConfig, level: ConfigLevel) -> Result<()> {
let parsed: Vec<(
&'static str,
Option<&'static gix::bstr::BStr>,
&'static str,
Vec<u8>,
)> = config
.entries
.iter()
.map(|(key, value)| {
let mut parts = key.splitn(3, '.');
let section: &'static str =
Box::leak(parts.next().unwrap_or("").to_owned().into_boxed_str());
let subsection: Option<&'static gix::bstr::BStr> = parts.next().map(|s| {
let bytes: &'static [u8] = Box::leak(s.as_bytes().to_vec().into_boxed_slice());
bytes.as_bstr()
});
let name: &'static str =
Box::leak(parts.next().unwrap_or("").to_owned().into_boxed_str());
(section, subsection, name, value.as_bytes().to_vec())
})
.collect();
self.try_gix_operation(|repo| {
let config_path = match level {
ConfigLevel::Local | ConfigLevel::Worktree => repo.git_dir().join("config"),
_ => {
return Err(anyhow::anyhow!(
"Only local config writing is supported with gix"
));
}
};
let bytes = if config_path.exists() {
std::fs::read(&config_path)?
} else {
Vec::new()
};
let mut config_file = gix_file_from_bytes(bytes).with_context(|| {
format!("Failed to read config file at {}", config_path.display())
})?;
for (section, subsection, name, value) in &parsed {
config_file.set_raw_value_by(*section, *subsection, *name, value.as_bstr())?;
}
let mut output = std::fs::File::create(&config_path)?;
config_file.write_to(&mut output)?;
Ok(())
})
}
fn set_config_value(&self, key: &str, value: &str, level: ConfigLevel) -> Result<()> {
let mut entries = HashMap::new();
entries.insert(key.to_string(), value.to_string());
let existing = self.read_git_config(level)?;
let mut merged = existing.entries;
merged.insert(key.to_string(), value.to_string());
let merged_config = GitConfig { entries: merged };
self.write_git_config(&merged_config, level)
}
fn add_submodule(&mut self, opts: &SubmoduleAddOptions) -> Result<()> {
Err(anyhow::anyhow!(
"gix add_submodule not implemented: use git2 or CLI fallback for '{}'",
opts.name
))
}
fn init_submodule(&mut self, path: &str) -> Result<()> {
let entries = self.read_gitmodules()?;
let submodule_entry = entries
.submodule_iter()
.find(|(_, entry)| entry.path.as_ref() == Some(&path.to_string()))
.ok_or_else(|| anyhow::anyhow!("Submodule '{path}' not found in .gitmodules"))?;
let (name, entry) = submodule_entry;
let url = entry
.url
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Submodule '{name}' has no URL configured"))?;
self.try_gix_operation(|repo| {
let config_snapshot = repo.config_snapshot();
let mut config_file = config_snapshot.to_owned();
let _url_key = format!("submodule.{name}.url");
config_file.set_raw_value_by(
"submodule",
Some(name.as_bytes().as_bstr()),
"url",
url.as_bytes().as_bstr(),
)?;
let _active_key = format!("submodule.{name}.active");
config_file.set_raw_value_by(
"submodule",
Some(name.as_bytes().as_bstr()),
"active",
b"true".as_bstr(),
)?;
let workdir = repo
.workdir()
.ok_or_else(|| anyhow::anyhow!("Repository has no working directory"))?;
let submodule_path = workdir.join(path);
if !submodule_path.exists() {
std::fs::create_dir_all(&submodule_path)?;
} else if submodule_path.read_dir()?.next().is_some() {
}
if !submodule_path.join(".git").exists() {
let mut prepare = gix::prepare_clone(url.clone(), &submodule_path)?;
if entry.shallow == Some(true) {
prepare = prepare
.with_shallow(gix::remote::fetch::Shallow::DepthAtRemote(1.try_into()?));
}
let should_interrupt =
std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let progress = gix::progress::Discard;
let (_checkout, _outcome) =
prepare.fetch_then_checkout(progress, &should_interrupt)?;
}
Ok(())
})
}
fn update_submodule(&mut self, path: &str, opts: &SubmoduleUpdateOptions) -> Result<()> {
let entries = self.read_gitmodules()?;
let submodule_entry = entries
.submodule_iter()
.find(|(_, entry)| entry.path.as_ref() == Some(&path.to_string()))
.ok_or_else(|| anyhow::anyhow!("Submodule '{path}' not found in .gitmodules"))?;
let (name, entry) = submodule_entry;
let url = entry
.url
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Submodule '{name}' has no URL configured"))?;
let workdir = self
.repo
.workdir()
.ok_or_else(|| anyhow::anyhow!("Repository has no working directory"))?;
let submodule_path = workdir.join(path);
if !submodule_path.exists() || !submodule_path.join(".git").exists() {
let mut prepare = gix::prepare_clone(url.clone(), &submodule_path)?;
if entry.shallow == Some(true) {
prepare =
prepare.with_shallow(gix::remote::fetch::Shallow::DepthAtRemote(1.try_into()?));
}
let should_interrupt = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let progress = gix::progress::Discard;
let (checkout, _outcome) = prepare.fetch_then_checkout(progress, &should_interrupt)?;
if let Some(branch) = &entry.branch {
let mut config_file = checkout.repo().config_snapshot().to_owned();
match branch {
crate::options::SerializableBranch::Name(branch_name) => {
config_file.set_raw_value_by(
"branch",
Some(branch_name.as_bytes().as_bstr()),
"remote",
b"origin".as_bstr(),
)?;
config_file.set_raw_value_by(
"branch",
Some(branch_name.as_bytes().as_bstr()),
"merge",
format!("refs/heads/{branch_name}").as_bytes().as_bstr(),
)?;
}
crate::options::SerializableBranch::CurrentInSuperproject => {
let superproject_branch = self.get_superproject_branch()?;
config_file.set_raw_value_by(
"branch",
Some(superproject_branch.as_bytes().as_bstr()),
"remote",
b"origin".as_bstr(),
)?;
config_file.set_raw_value_by(
"branch",
Some(superproject_branch.as_bytes().as_bstr()),
"merge",
format!("refs/heads/{superproject_branch}")
.as_bytes()
.as_bstr(),
)?;
}
}
}
} else {
let submodule_repo = gix::open(&submodule_path)?;
fetch_repo(submodule_repo, None, entry.shallow == Some(true))
.map_err(|e| anyhow::anyhow!("Failed to fetch submodule: {e}"))?;
match opts.strategy {
crate::options::SerializableUpdate::Checkout
| crate::options::SerializableUpdate::Unspecified => {
}
crate::options::SerializableUpdate::Merge => {
return Err(anyhow::anyhow!(
"Merge strategy not yet implemented with gix"
));
}
crate::options::SerializableUpdate::Rebase => {
return Err(anyhow::anyhow!(
"Rebase strategy not yet implemented with gix"
));
}
crate::options::SerializableUpdate::None => {
}
}
}
Ok(())
}
fn delete_submodule(&mut self, path: &str) -> Result<()> {
let mut entries = self.read_gitmodules()?;
let submodule_name = entries
.submodule_iter()
.find(|(_, entry)| entry.path.as_ref() == Some(&path.to_string()))
.map(|(name, _)| name.to_string())
.ok_or_else(|| anyhow::anyhow!("Submodule '{path}' not found in .gitmodules"))?;
entries.remove_submodule(&submodule_name);
self.write_gitmodules(&entries)?;
self.try_gix_operation_mut(|repo| {
let index_path = repo.git_dir().join("index");
if index_path.exists() {
let mut index = gix::index::File::at(
&index_path,
gix::hash::Kind::Sha1,
false,
gix::index::decode::Options::default(),
)?;
let remove_prefix = path;
index.remove_entries(|_idx, path, _entry| {
let path_str = std::str::from_utf8(path).unwrap_or("");
path_str.starts_with(remove_prefix)
});
let mut index_file = std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(&index_path)?;
index.write_to(&mut index_file, gix::index::write::Options::default())?;
let mut index_file = std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(&index_path)?;
index.write_to(&mut index_file, gix::index::write::Options::default())?;
}
let config_snapshot = repo.config_snapshot();
let _config_file = config_snapshot.to_owned();
let _config_file = config_snapshot.to_owned();
let _section_name = format!("submodule.{submodule_name}");
let _section_name = format!("submodule.{submodule_name}");
let workdir = repo
.workdir()
.ok_or_else(|| anyhow::anyhow!("Repository has no working directory"))?;
let submodule_path = workdir.join(path);
if submodule_path.exists() {
std::fs::remove_dir_all(&submodule_path).with_context(|| {
format!(
"Failed to remove submodule directory at {}",
submodule_path.display()
)
})?;
}
let modules_path = repo.git_dir().join("modules").join(&submodule_name);
if modules_path.exists() {
std::fs::remove_dir_all(&modules_path).with_context(|| {
format!(
"Failed to remove submodule git directory at {}",
modules_path.display()
)
})?;
}
Ok(())
})
}
fn deinit_submodule(&mut self, path: &str, force: bool) -> Result<()> {
let entries = self.read_gitmodules()?;
let submodule_name = entries
.submodule_iter()
.find(|(_, entry)| entry.path.as_ref() == Some(&path.to_string()))
.map(|(name, _)| name.to_string())
.ok_or_else(|| anyhow::anyhow!("Submodule '{path}' not found in .gitmodules"))?;
self.clone().try_gix_operation_mut(|repo| {
let workdir = repo.workdir()
.ok_or_else(|| anyhow::anyhow!("Repository has no working directory"))?;
let submodule_path = workdir.join(path);
if !force && submodule_path.exists() && submodule_path.join(".git").exists() {
if let Ok(submodule_repo) = gix::open(&submodule_path) {
match submodule_repo.is_dirty() {
Ok(is_dirty) => {
if is_dirty {
return Err(anyhow::anyhow!(
"Submodule '{path}' has uncommitted changes. Use force=true to override."
));
}
}
Err(err) => {
return Err(anyhow::anyhow!(
"Submodule '{path}' might have uncommitted changes. Use force=true to override.\nError: {err}"
));
}
}
} else {
return Err(anyhow::anyhow!(
"Submodule '{path}' might have uncommitted changes. Use force=true to override."
));
}
}
let config_snapshot = repo.config_snapshot();
let _config_file = config_snapshot.to_owned();
let _config_file = config_snapshot.to_owned();
if submodule_path.exists() {
if force {
std::fs::remove_dir_all(&submodule_path)
.with_context(|| format!("Failed to remove submodule directory at {}", submodule_path.display()))?;
std::fs::create_dir_all(&submodule_path)?;
} else {
let git_dir = submodule_path.join(".git");
if git_dir.exists() {
if git_dir.is_dir() {
std::fs::remove_dir_all(&git_dir)?;
} else {
std::fs::remove_file(&git_dir)?;
}
}
for entry in std::fs::read_dir(&submodule_path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
std::fs::remove_file(&path).ok(); }
}
}
}
let modules_path = repo.git_dir().join("modules").join(&submodule_name);
if modules_path.exists() {
std::fs::remove_dir_all(&modules_path)
.with_context(|| format!("Failed to remove submodule git directory at {}", modules_path.display()))?;
}
Ok(())
})
}
fn get_submodule_status(&self, _path: &str) -> Result<DetailedSubmoduleStatus> {
Err(anyhow::anyhow!(
"get_submodule_status not yet implemented with gix"
))
}
fn list_submodules(&self) -> Result<Vec<String>> {
self.try_gix_operation(|repo| {
let mut submodule_paths = Vec::new();
if let Some(submodule_iter) = repo.submodules()? {
for submodule in submodule_iter {
let path = submodule.path()?.to_string();
submodule_paths.push(path);
}
}
Ok(submodule_paths)
})
}
fn fetch_submodule(&self, _path: &str) -> Result<()> {
let submodule_repo = utilities::repo_from_path(&std::path::PathBuf::from(_path))?;
fetch_repo(submodule_repo, None, false)
.map_err(|e| anyhow::anyhow!("Failed to fetch submodule: {e}"))
}
fn reset_submodule(&self, _path: &str, _hard: bool) -> Result<()> {
Err(anyhow::anyhow!(
"gix submodule reset not yet supported, falling back to git2"
))
}
fn clean_submodule(&self, _path: &str, _force: bool, _remove_directories: bool) -> Result<()> {
Err(anyhow::anyhow!(
"gix submodule cleaning not yet supported, falling back to git2"
))
}
fn stash_submodule(&self, _path: &str, _include_untracked: bool) -> Result<()> {
Err(anyhow::anyhow!(
"gix stashing not yet supported, falling back to git2"
))
}
fn enable_sparse_checkout(&self, _path: &str) -> Result<()> {
Err(anyhow::anyhow!(
"gix sparse checkout setup not implemented for submodule paths, falling back to git2"
))
}
fn set_sparse_patterns(&self, _path: &str, _patterns: &[String]) -> Result<()> {
Err(anyhow::anyhow!(
"gix sparse patterns not implemented for submodule paths, falling back to git2"
))
}
fn get_sparse_patterns(&self, _path: &str) -> Result<Vec<String>> {
Err(anyhow::anyhow!(
"gix get sparse patterns not implemented for submodule paths, falling back to git2"
))
}
fn apply_sparse_checkout(&self, _path: &str) -> Result<()> {
self.try_gix_operation(|repo| {
let patterns = self.get_sparse_patterns(_path)?;
if patterns.is_empty() {
return Ok(()); }
let index_path = repo.git_dir().join("index");
let _index = gix::index::File::at(
&index_path,
gix::hash::Kind::Sha1,
false,
gix::index::decode::Options::default(),
)?;
Err(anyhow::anyhow!(
"gix sparse checkout application is complex, falling back to git2"
))
})
}
}
impl From<super::GitOpsManager> for GixOperations {
fn from(git_ops: super::GitOpsManager) -> Self {
git_ops
.gix_ops
.expect("GixOperations should always be initialized")
}
}